fix: apply description and default metadata to enum, const, and not schemas in fromJSONSchema#5758
Conversation
… fromJSONSchema Fixes colinhacks#5732 Early returns for enum, const, and not: {} (never) schemas in convertBaseSchema() bypassed the metadata application block at the end of the function. Extract metadata application into a helper function applyBaseMetadata() and call it from each of the affected early-return paths so that description and default are consistently honored across all schema types.
Move `description` and `default` handling out of `convertBaseSchema` and
into a single application site at the end of `convertSchema`, after
composition keywords (anyOf/oneOf/allOf) and wrappers (nullable/readOnly)
have been applied.
This eliminates the original bug class — early returns in
`convertBaseSchema` for `enum`, `const`, and `not: {}` silently dropped
description/default — without needing per-branch helper calls. As a
side effect it also fixes the same latent bug for the `type: [...]`
array expansion path.
Apply order is `default` → extraMeta → `describe`, so the `.describe()`
clone sits at the top of the parent chain. `_zod.parent` inheritance in
`$ZodRegistry.get()` then keeps `extraMeta` reachable from the returned
reference, while `schema.description` continues to resolve via
globalRegistry as before.
Adds tests for description/default on `type: [...]` arrays and for the
description+default+anyOf composition cases.
|
TL;DR — Key changes
Summary | 2 files | 3 commits | base: Metadata application lifted to
|
# Conflicts: # packages/zod/src/v4/classic/from-json-schema.ts
| } | ||
|
|
||
| // Collect metadata: core schema keywords and unrecognized keys | ||
| // Apply `default` so it wraps the fully-composed schema. This ensures |
There was a problem hiding this comment.
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)
|
Merged. Pushed a small follow-up on top: rather than calling Note: this comment was produced by an AI coding assistant. |
|
Landed in Zod 4.4 |

Problem
z.fromJSONSchema()ignoresdescriptionanddefaultfields when the JSON Schema usesenum,const, ornot: {}(never). This happens because these paths use early returns inconvertBaseSchema()before reaching the metadata application block at the bottom of the function.Minimal reproduction:
Fixes #5732
Solution
Extract the metadata application (description + default) into a helper function
applyBaseMetadata()and call it from each of the affected early-return paths:not: {}→z.never()enum→z.enum()/z.literal()/z.union()const→z.literal()Tests
Added five new tests covering:
descriptionon enum schemadescriptionon const schemadescriptionon not: {} (never) schemadefaulton enum schemadefaulton const schema