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 AxFieldType enum (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, boolean via mapAxTypeToJsonSchemaType
  • Arrays: Creates items schemas, recursing into nested objects when detected
  • Objects: Builds nested properties objects, marking required fields and validating that media types (image, audio, file) never appear inside nested structures (lines 34-45)
  • Classifications: Translates class types 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" with enum array

Validation constraints attach directly to schema properties:

  • minLength/maxLength for strings
  • minimum/maximum for numbers
  • pattern for regex validation
  • format for 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 items schema
  • 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:

  1. The signature's toJSONSchema() generates the output schema
  2. For function-calling providers, this becomes the parameters field in the function definition
  3. 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 AxField objects, recursive conversion via fieldToSchema, and validation enhancement.
  • Parsing occurs in src/ax/dsp/parser.ts, where parseSignature extracts field metadata including types, descriptions, and constraints.
  • Conversion happens in src/ax/dsp/jsonSchema.ts, where toJsonSchema and fieldToSchema map 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 AxGenerator in src/ax/dsp/generate.ts passes 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →