Model Capabilities
Structured Outputs
Structured Outputs lets the API return responses in a specific format, for example, a JSON object matching a schema you define instead of free-form text. This feature is especially useful for tasks like document parsing, entity extraction, and report generation.
When using supported schema features, the response is guaranteed to match your schema.
There are two ways to request structured outputs from the model.
The primary and most flexible method is to use the response_format parameter. By setting response_format.type to "json_schema" and providing your schema under response_format.json_schema, you can define exactly what structured output the model should return. The parameter also accepts "json_object" for any well-formed JSON when you don't need a specific structure, or "text" (the default) for free-form text.
The second way is through tool calling. When you define tools, xAI models will always generate tool call arguments that strictly conform to the tool’s input JSON Schema (the strict flag is implicitly always true).
Tool schemas follow the same JSON Schema support rules described on this page. See the Function Calling documentation for full details.
You can define schemas using libraries like Pydantic or Zod.
JSON Schema support
We support a practical subset of JSON Schema. Schemas authored against Draft 2020-12 work best; Draft-07 schemas are also accepted.
Supported types
stringnumberintegerbooleannullenumconstarrayobjectanyOfoneOf(behaves identically toanyOf)allOf(single subschema only; see Best-effort keywords for multiple)$ref/$defs(non-circular references only)
additionalProperties defaults to false and must be set to true explicitly.
To make a field nullable, use a type array ({"type": ["string", "null"]}) or an anyOf variant that includes null. Fields not listed in required are treated as optional.
String formats
The format keyword is enforced for these values:
date · time · date-time · email · uuid · ipv4 · ipv6 · uri
Other format values are accepted but not enforced (see Best-effort keywords).
Constraint limits
The following constraints are enforced by the output engine up to the thresholds below. Schemas exceeding these limits are still accepted, but conformance relies on model behavior.
| Keyword | Guaranteed up to |
|---|---|
minimum / maximum / exclusiveMinimum / exclusiveMaximum | No limit |
minLength / maxLength | 2,048 |
minItems / maxItems | 256 |
minProperties / maxProperties | 64 |
Best-effort keywords
These keywords are accepted but not structurally enforced; the model handles them and does so reliably in practice, but outputs are not guaranteed to satisfy these constraints. We recommend validating if strict conformance is required.
notif/then/elseallOfwith more than one subschemaformatvalues not listed under String formats- Constraints exceeding the limits above
Rejected schemas
The following will return a 400 error:
enumoranyOfwith zero variants- Properties with a schema of
trueorfalse maxContains/minContainsitemsas an array (useprefixItemsfor tuple validation)
Regex support (pattern)
When using the pattern keyword on a string field, we support a practical subset of ECMAScript Regular Expressions (ECMA-262).
Supported:
- Literals and character classes (
[abc],[a-z],[^abc]) .(matches any Unicode codepoint, including newlines)- Alternation
|, grouping(...), and non-capturing groups(?:...) - Quantifiers
*,+,?and repetition ranges{n},{n,},{n,m} - Shorthand classes
\d,\w,\s(and their negations\D,\W,\S) - Common escapes:
\n,\t,\r,\f,\xHH,\uHHHH,\u{HHHHHH}
Not supported:
- Backreferences (
\1,\k<name>, etc.) - Unicode property escapes (
\p{L},\P{Letter}) - Word boundaries (
\b,\B) - Lookahead and lookbehind (
(?=...),(?<=...), etc.) - Inline modifiers (
(?i),(?m), etc.) - Conditional expressions and other advanced constructs
Semantic differences from standard JavaScript RegExp:
.matches newlines^and$are implicit—the pattern always matches the entire string (no need to add them)- Capturing groups
(...)have no semantic effect (they behave like non-capturing groups) - The regex is evaluated with Unicode support
Example: Invoice Parsing
A common use case for Structured Outputs is parsing raw documents. For example, invoices contain structured data like vendor details, amounts, and dates, but extracting this data from raw text can be error-prone. Structured Outputs ensure the extracted data matches a predefined schema.
Let's say you want to extract the following data from an invoice:
- Vendor name and address
- Invoice number and date
- Line items (description, quantity, price)
- Total amount and currency
We'll use structured outputs to have Grok generate a strongly-typed JSON for this.
Step 1: Defining the Schema
You can use Pydantic or Zod to define your schema.
from datetime import date
from enum import Enum
from pydantic import BaseModel, Field
class Currency(str, Enum):
USD = "USD"
EUR = "EUR"
GBP = "GBP"
class LineItem(BaseModel):
description: str = Field(description="Description of the item or service")
quantity: int = Field(description="Number of units", ge=1)
unit_price: float = Field(description="Price per unit", ge=0)
class Address(BaseModel):
street: str = Field(description="Street address")
city: str = Field(description="City")
postal_code: str = Field(description="Postal/ZIP code")
country: str = Field(description="Country")
class Invoice(BaseModel):
vendor_name: str = Field(description="Name of the vendor")
vendor_address: Address = Field(description="Vendor's address")
invoice_number: str = Field(description="Unique invoice identifier")
invoice_date: date = Field(description="Date the invoice was issued")
line_items: list[LineItem] = Field(description="List of purchased items/services")
total_amount: float = Field(description="Total amount due", ge=0)
currency: Currency = Field(description="Currency of the invoice")Step 2: Prepare The Prompts
System Prompt
The system prompt instructs the model to extract invoice data from text. Since the schema is defined separately, the prompt can focus on the task without explicitly specifying the required fields in the output JSON.
Text
Given a raw invoice, carefully analyze the text and extract the relevant invoice data into JSON format.
Example Invoice Text
Text
Vendor: Acme Corp, 123 Main St, Springfield, IL 62704
Invoice Number: INV-2025-001
Date: 2025-02-10
Items:
- Widget A, 5 units, $10.00 each
- Widget B, 2 units, $15.00 each
Total: $80.00 USD
Step 3: The Final Code
Use the structured outputs feature of the SDK to parse the invoice.
import os
from datetime import date
from enum import Enum
from pydantic import BaseModel, Field
from xai_sdk import Client
from xai_sdk.chat import system, user
# Pydantic Schemas
class Currency(str, Enum):
USD = "USD"
EUR = "EUR"
GBP = "GBP"
class LineItem(BaseModel):
description: str = Field(description="Description of the item or service")
quantity: int = Field(description="Number of units", ge=1)
unit_price: float = Field(description="Price per unit", ge=0)
class Address(BaseModel):
street: str = Field(description="Street address")
city: str = Field(description="City")
postal_code: str = Field(description="Postal/ZIP code")
country: str = Field(description="Country")
class Invoice(BaseModel):
vendor_name: str = Field(description="Name of the vendor")
vendor_address: Address = Field(description="Vendor's address")
invoice_number: str = Field(description="Unique invoice identifier")
invoice_date: date = Field(description="Date the invoice was issued")
line_items: list[LineItem] = Field(description="List of purchased items/services")
total_amount: float = Field(description="Total amount due", ge=0)
currency: Currency = Field(description="Currency of the invoice")
client = Client(api_key=os.getenv("XAI_API_KEY"))
chat = client.chat.create(model="grok-4.20-reasoning")
chat.append(system("Given a raw invoice, carefully analyze the text and extract the invoice data into JSON format."))
chat.append(
user("""
Vendor: Acme Corp, 123 Main St, Springfield, IL 62704
Invoice Number: INV-2025-001
Date: 2025-02-10
Items: - Widget A, 5 units, $10.00 each - Widget B, 2 units, $15.00 each
Total: $80.00 USD
""")
)
# The parse method returns a tuple of the full response object as well as the parsed pydantic object.
response, invoice = chat.parse(Invoice)
assert isinstance(invoice, Invoice)
# Can access fields of the parsed invoice object directly
print(invoice.vendor_name)
print(invoice.invoice_number)
print(invoice.invoice_date)
print(invoice.line_items)
print(invoice.total_amount)
print(invoice.currency)
# Can also access fields from the raw response object such as the content.
# In this case, the content is the JSON schema representation of the parsed invoice object
print(response.content)Step 4: Type-safe Output
When using supported schema features, the output will be type-safe and respect the input schema.
JSON
{
"vendor_name": "Acme Corp",
"vendor_address": {
"street": "123 Main St",
"city": "Springfield",
"postal_code": "62704",
"country": "IL"
},
"invoice_number": "INV-2025-001",
"invoice_date": "2025-02-10",
"line_items": [
{ "description": "Widget A", "quantity": 5, "unit_price": 10.0 },
{ "description": "Widget B", "quantity": 2, "unit_price": 15.0 }
],
"total_amount": 80.0,
"currency": "USD"
}
Structured Outputs with Tools
Structured outputs with tools is only available for supported Grok 4 family models.
You can combine structured outputs with tool calling to get type-safe responses from tool-augmented queries. This works with both:
- Agentic tool calling: Server-side tools like web search, X search, and code execution that the model orchestrates autonomously.
- Function calling: User-supplied tools where you define custom functions and handle tool execution yourself.
This combination enables workflows where the model can use tools to gather information and return results in a predictable, strongly-typed format.
Example: Agentic Tools with Structured Output
This example uses web search to find the latest research on a topic and extracts structured data into a schema:
from pydantic import BaseModel, Field
class ProofInfo(BaseModel):
name: str = Field(description="Name of the proof or paper")
authors: str = Field(description="Authors of the proof")
year: str = Field(description="Year published")
summary: str = Field(description="Brief summary of the approach")
import os
from pydantic import BaseModel, Field
from xai_sdk import Client
from xai_sdk.chat import user
from xai_sdk.tools import web_search
# ProofInfo schema defined above
client = Client(api_key=os.getenv("XAI_API_KEY"))
chat = client.chat.create(
model="grok-4.20-reasoning",
tools=[web_search()],
)
chat.append(user("Find the latest machine-checked proof of the four color theorem."))
response, proof = chat.parse(ProofInfo)
print(f"Name: {proof.name}")
print(f"Authors: {proof.authors}")
print(f"Year: {proof.year}")
print(f"Summary: {proof.summary}")
Example: Client-side Tools with Structured Output
This example uses a client-side function tool to compute Collatz sequence steps and returns the result in a structured format:
from pydantic import BaseModel, Field
class CollatzResult(BaseModel):
starting_number: int = Field(description="The input number")
steps: int = Field(description="Number of steps to reach 1")
import os
import json
from pydantic import BaseModel, Field
from xai_sdk import Client
from xai_sdk.chat import tool, tool_result, user
# CollatzResult schema defined above
def collatz_steps(n: int) -> int:
"""Returns the number of steps for n to reach 1 in the Collatz sequence."""
steps = 0
while n != 1:
n = n // 2 if n % 2 == 0 else 3 * n + 1
steps += 1
return steps
collatz_tool = tool(
name="collatz_steps",
description="Compute the number of steps for a number to reach 1 in the Collatz sequence",
parameters={
"type": "object",
"properties": {
"n": {"type": "integer", "description": "The starting number"},
},
"required": ["n"],
},
)
client = Client(api_key=os.getenv("XAI_API_KEY"))
chat = client.chat.create(
model="grok-4.20-reasoning",
tools=[collatz_tool],
)
chat.append(user("Use the collatz_steps tool to find how many steps it takes for 20250709 to reach 1."))
# Handle tool calls until we get a final response
while True:
response = chat.sample()
if not response.tool_calls:
break
chat.append(response)
for tc in response.tool_calls:
args = json.loads(tc.function.arguments)
result = collatz_steps(args["n"])
chat.append(tool_result(str(result)))
# Parse the final response into structured output
response, result = chat.parse(CollatzResult)
print(f"Starting number: {result.starting_number}")
print(f"Steps to reach 1: {result.steps}")
Alternative: Using response_format with sample() or stream()
When using the xAI Python SDK, there's an alternative way to retrieve structured outputs. Instead of using the parse() method, you can pass your Pydantic model directly to the response_format parameter when creating a chat, and then use sample() or stream() to get the response.
How It Works
When you pass a Pydantic model to response_format, the SDK automatically:
- Converts your Pydantic model to a JSON schema
- Constrains the model's output to conform to that schema
- Returns the response as a JSON string, that is conforming to the Pydantic model, in
response.content
You then manually parse the JSON string into your Pydantic model instance.
Key Differences
| Approach | Method | Returns | Parsing |
|---|---|---|---|
Using parse() | chat.parse(Model) | Tuple of (Response, Model) | Automatic - SDK parses for you |
Using response_format | chat.sample() or chat.stream() | Response with JSON string | Manual - You parse response.content |
When to Use Each Approach
- Use
parse()when you want the simplest, most convenient experience with automatic parsing - Use
response_format+sample()orstream()when you:- Want more control over the parsing process
- Need to handle the raw JSON string before parsing
- Want to use streaming with structured outputs
- Are integrating with existing code that expects to work with
sample()orstream()
Example Using response_format
Python
import os
from datetime import date
from enum import Enum
from pydantic import BaseModel, Field
from xai_sdk import Client
from xai_sdk.chat import system, user
# Pydantic Schemas
class Currency(str, Enum):
USD = "USD"
EUR = "EUR"
GBP = "GBP"
class LineItem(BaseModel):
description: str = Field(description="Description of the item or service")
quantity: int = Field(description="Number of units", ge=1)
unit_price: float = Field(description="Price per unit", ge=0)
class Address(BaseModel):
street: str = Field(description="Street address")
city: str = Field(description="City")
postal_code: str = Field(description="Postal/ZIP code")
country: str = Field(description="Country")
class Invoice(BaseModel):
vendor_name: str = Field(description="Name of the vendor")
vendor_address: Address = Field(description="Vendor's address")
invoice_number: str = Field(description="Unique invoice identifier")
invoice_date: date = Field(description="Date the invoice was issued")
line_items: list[LineItem] = Field(description="List of purchased items/services")
total_amount: float = Field(description="Total amount due", ge=0)
currency: Currency = Field(description="Currency of the invoice")
client = Client(api_key=os.getenv("XAI_API_KEY"))
# Pass the Pydantic model to response_format instead of using parse()
chat = client.chat.create(
model="grok-4.20-reasoning",
response_format=Invoice, # Pass the Pydantic model here
)
chat.append(system("Given a raw invoice, carefully analyze the text and extract the invoice data into JSON format."))
chat.append(
user("""
Vendor: Acme Corp, 123 Main St, Springfield, IL 62704
Invoice Number: INV-2025-001
Date: 2025-02-10
Items: - Widget A, 5 units, $10.00 each - Widget B, 2 units, $15.00 each
Total: $80.00 USD
""")
)
# Use sample() instead of parse() - returns Response object
response = chat.sample()
# The response.content is a valid JSON string conforming to your schema
print(response.content)
# Output: {"vendor_name": "Acme Corp", "vendor_address": {...}, ...}
# Manually parse the JSON string into your Pydantic model
invoice = Invoice.model_validate_json(response.content)
assert isinstance(invoice, Invoice)
# Access fields of the parsed invoice object
print(invoice.vendor_name)
print(invoice.invoice_number)
print(invoice.total_amount)
Streaming with Structured Outputs
You can also use stream() with response_format to get streaming structured output. The chunks will progressively build up the JSON string:
Python
import os
from pydantic import BaseModel, Field
from xai_sdk import Client
from xai_sdk.chat import system, user
class Summary(BaseModel):
title: str = Field(description="A brief title")
key_points: list[str] = Field(description="Main points from the text")
sentiment: str = Field(description="Overall sentiment: positive, negative, or neutral")
client = Client(api_key=os.getenv("XAI_API_KEY"))
chat = client.chat.create(
model="grok-4.20-reasoning",
response_format=Summary, # Pass the Pydantic model here
)
chat.append(system("Analyze the following text and provide a structured summary."))
chat.append(user("The new product launch exceeded expectations with record sales..."))
# Stream the response - chunks contain partial JSON
for response, chunk in chat.stream():
print(chunk.content, end="", flush=True)
# Parse the complete JSON string into your model
summary = Summary.model_validate_json(response.content)
print(f"Title: {summary.title}")
print(f"Sentiment: {summary.sentiment}")
Did you find this page helpful?
Last updated: April 23, 2026