How Ax Generates JSON Schema from Signatures for Structured Output
Ax generates JSON Schema from signatures by parsing declarative field definitions into AxField objects, recursively converting them to JSON Schema types via fieldToSchema, and enhancing descriptions with validation constraints to guide LLM structured output.
The Ax framework (ax-llm/ax) enables type-safe structured output from large language models by transforming developer-friendly signatures into rigorous JSON Schemas. This process bridges the gap between human-readable field definitions and the strict schema requirements of function-calling LLM APIs.
Parsing Signatures into AxField Objects
The journey from signature to schema begins with parsing the declarative definition into structured metadata objects.
The parseSignature Function
In src/ax/dsp/parser.ts, the parseSignature function tokenizes signature strings—whether provided as raw text, an AxSignature instance, or a fluent builder (f()). It extracts field names, descriptions, type annotations, and modifiers.
For example, a signature like userQuery:string "Question" -> answer:string, confidence:number is parsed into separate input and output AxField arrays.
AxField Structure and Modifiers
Each AxField object captured in src/ax/dsp/sig.ts contains:
- name: The field identifier
- description: Human-readable context for the LLM
- type: An
AxFieldTypeenum (string, number, boolean, array, object, class, date, etc.) - Modifiers:
isOptional,isInternal(excluded from schemas), and validation constraints (minLength,maximum,pattern,format)
These modifiers directly influence the final JSON Schema constraints.
Converting AxField to JSON Schema
Once parsed, the AxField objects undergo recursive transformation into JSON Schema definitions.
The toJSONSchema Method
The AxSignature class in src/ax/dsp/sig.ts (lines 39-44) exposes toJSONSchema(), which concatenates input and output fields and delegates to toJsonSchema in src/ax/dsp/jsonSchema.ts.
For generators requiring input validation, toInputJSONSchema() filters for input fields only, producing schemas suitable for request validation.
Recursive Schema Building with fieldToSchema
At line 95 of src/ax/dsp/jsonSchema.ts, the toJsonSchema function initializes a root object schema and iterates over fields. For each field, it calls fieldToSchema, a recursive function handling:
- Primitives: Maps
string,number,booleanviamapAxTypeToJsonSchemaType - Arrays: Creates
itemsschemas, recursing into nested objects when detected - Objects: Builds nested
propertiesobjects, marking required fields and validating that media types (image, audio, file) never appear inside nested structures (lines 34-45) - Classifications: Translates
classtypes into string enums
Type Mapping and Validation Constraints
The mapAxTypeToJsonSchemaType function handles Ax-specific types:
string→"type": "string"number→"type": "number"boolean→"type": "boolean"date→"type": "string", "format": "date"email→"type": "string", "format": "email"class→"type": "string"withenumarray
Validation constraints attach directly to schema properties:
minLength/maxLengthfor stringsminimum/maximumfor numberspatternfor regex validationformatfor semantic types (email, date, uri)
Description Enhancement for LLM Guidance
Before finalizing the schema, enhanceDescriptionWithValidation (lines 5-92 in src/ax/dsp/jsonSchema.ts) augments field descriptions with human-readable validation rules. For example, a string field with minLength: 5 receives the appended text: "Minimum length: 5 characters."
This enrichment ensures the LLM receives explicit guidance about constraints without requiring separate documentation.
Schema Validation and Generator Integration
The generated schema undergoes final validation before reaching the LLM provider.
validateJSONSchema Guards
The validateJSONSchema function (lines 44-63 in src/ax/dsp/jsonSchema.ts) performs structural checks:
- Verifies every array defines an
itemsschema - Confirms nested objects contain valid property definitions
- Ensures media types (image, audio, file) only appear at root level
These guards prevent runtime errors when schemas are passed to LLM APIs.
Integration with AxGenerator
In src/ax/dsp/generate.ts, the AxGenerator class utilizes the schema at lines 426 and 743. When executing a generation:
- The signature's
toJSONSchema()generates the output schema - For function-calling providers, this becomes the
parametersfield in the function definition - The LLM receives the schema and returns JSON conforming to the exact structure
This integration enables type-safe structured output without manual schema writing.
Practical Examples
Building a Signature with Validation Constraints
import { f, ax } from '@ax-llm/ax';
// Define a signature with constraints
const signature = f()
.input('email', f.string().email())
.input('age', f.number().min(18).max(120))
.input('tags', f.string('Tag name').array().min(1).max(5))
.output('profile', f.object({
name: f.string('Full name').min(3).max(50),
birthday: f.date(),
preferences: f.object({
newsletter: f.boolean(),
theme: f.class(['light', 'dark'] as const)
})
}))
.build();
// Convert to JSON Schema
const schema = signature.toJSONSchema();
console.log(JSON.stringify(schema, null, 2));
Result (abridged):
{
"type": "object",
"title": "Schema",
"properties": {
"email": {
"type": "string",
"format": "email",
"description": "Must be a valid email address format."
},
"age": {
"type": "number",
"minimum": 18,
"maximum": 120
},
"tags": {
"type": "array",
"items": {
"type": "string",
"minLength": 1,
"maxLength": 5
}
},
"profile": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 3,
"maxLength": 50,
"description": "Full name. Minimum length: 3 characters, maximum length: 50 characters."
},
"birthday": {
"type": "string",
"format": "date",
"description": "Format: YYYY-MM-DD."
},
"preferences": {
"type": "object",
"properties": {
"newsletter": { "type": "boolean" },
"theme": {
"type": "string",
"enum": ["light", "dark"]
}
},
"required": ["newsletter", "theme"],
"additionalProperties": false
}
},
"required": ["name", "birthday", "preferences"],
"additionalProperties": false
}
},
"required": ["email", "age", "tags", "profile"],
"additionalProperties": false
}
Using the Schema in a Generator
import { ax, agent, s } from '@ax-llm/ax';
import { ai } from '@ax-llm/ax-ai-sdk-provider';
const llm = ai({ name: 'openai', apiKey: process.env.OPENAI_APIKEY! });
const sig = s(`
userQuery:string "Question from the user"
->
answer:string "Answer to the question"
confidence:number "0‑1 confidence score"
`);
const myAgent = agent(sig, {
name: 'qaAgent',
description: 'Answer user questions with confidence',
definition: 'You are a helpful Q&A assistant.',
ai: llm,
structured: true,
});
const result = await myAgent.run({ userQuery: 'What is the capital of France?' });
console.log(result);
// → { answer: 'Paris', confidence: 0.99 }
The agent automatically calls sig.toJSONSchema() and sends that schema to the OpenAI function-calling API, ensuring the model returns an object matching the signature exactly.
Summary
- Ax generates JSON Schema from signatures through a three-stage pipeline: parsing into
AxFieldobjects, recursive conversion viafieldToSchema, and validation enhancement. - Parsing occurs in
src/ax/dsp/parser.ts, whereparseSignatureextracts field metadata including types, descriptions, and constraints. - Conversion happens in
src/ax/dsp/jsonSchema.ts, wheretoJsonSchemaandfieldToSchemamap Ax types to JSON Schema primitives, handle nested objects/arrays, and attach validation constraints. - Description enhancement adds human-readable validation rules to field descriptions, giving LLMs explicit guidance on constraints like minimum length or numeric ranges.
- Integration with
AxGeneratorinsrc/ax/dsp/generate.tspasses the final schema to LLM providers as function parameters, enabling type-safe structured output without manual schema authoring.
Frequently Asked Questions
How does Ax handle nested objects when generating JSON Schema?
Ax recursively processes nested objects through the fieldToSchema function in src/ax/dsp/jsonSchema.ts. When encountering an object type, it builds a nested properties definition, marks required fields, and validates that media types (image, audio, file) only appear at the root level. The recursion handles arbitrary nesting depth for complex data structures.
What validation constraints does Ax support in JSON Schema generation?
Ax supports comprehensive validation constraints including minLength/maxLength for strings, minimum/maximum for numbers, pattern for regex validation, and format for semantic types like email and date. These constraints attach directly to JSON Schema properties, and enhanceDescriptionWithValidation appends human-readable explanations to field descriptions to guide the LLM.
Can I use the generated JSON Schema for input validation as well as output?
Yes. The AxSignature class provides both toJSONSchema() for output fields and toInputJSONSchema() for input fields. When using ax() or new AxGen, the framework automatically passes the appropriate schema to the LLM provider's function-calling API, ensuring the model receives strict typing for both request parameters and response structures.
Where does the schema validation happen before sending to the LLM?
Schema validation occurs in validateJSONSchema within src/ax/dsp/jsonSchema.ts (lines 44-63). This function verifies that every array defines an items schema, confirms nested objects contain valid property definitions, and ensures media types are correctly positioned. These guards prevent runtime errors when schemas are transmitted to LLM providers like OpenAI or Anthropic.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →