Model Capabilities

Structured Outputs

View as Markdown

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

  • string
  • number
  • integer
  • boolean
  • null
  • enum
  • const
  • array
  • object
  • anyOf
  • oneOf (behaves identically to anyOf)
  • 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.

KeywordGuaranteed up to
minimum / maximum / exclusiveMinimum / exclusiveMaximumNo limit
minLength / maxLength2,048
minItems / maxItems256
minProperties / maxProperties64

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.

  • not
  • if / then / else
  • allOf with more than one subschema
  • format values not listed under String formats
  • Constraints exceeding the limits above

Rejected schemas

The following will return a 400 error:

  • enum or anyOf with zero variants
  • Properties with a schema of true or false
  • maxContains / minContains
  • items as an array (use prefixItems for 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:

  1. Converts your Pydantic model to a JSON schema
  2. Constrains the model's output to conform to that schema
  3. 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

ApproachMethodReturnsParsing
Using parse()chat.parse(Model)Tuple of (Response, Model)Automatic - SDK parses for you
Using response_formatchat.sample() or chat.stream()Response with JSON stringManual - 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() or stream() 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() or stream()

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