Skip to main content
Core AI provides strongly-typed structured outputs using Zod schemas, ensuring your responses match expected formats with full TypeScript type inference.

Generate Structured Objects

Use generateObject() to get validated JSON responses:
import { generateObject } from '@core-ai/core-ai';
import { createOpenAI } from '@core-ai/openai';
import { z } from 'zod';

const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const model = openai.chatModel('gpt-5-mini');

const weatherSchema = z.object({
  city: z.string(),
  temperatureC: z.number(),
  summary: z.string(),
});

const result = await generateObject({
  model,
  messages: [
    {
      role: 'user',
      content: 'Return a weather report for Berlin as structured JSON.',
    },
  ],
  schema: weatherSchema,
  schemaName: 'weather_report',
  schemaDescription: 'A structured weather report object.',
});

// result.object is fully typed!
console.log('City:', result.object.city);
console.log('Temperature:', result.object.temperatureC);
console.log('Summary:', result.object.summary);

Stream Structured Objects

Stream JSON as it’s being generated:
import { streamObject } from '@core-ai/core-ai';
import { z } from 'zod';

const extractSchema = z.object({
  headline: z.string(),
  sentiment: z.enum(['positive', 'neutral', 'negative']),
  tags: z.array(z.string()),
});

const result = await streamObject({
  model,
  messages: [
    {
      role: 'user',
      content: 'Analyze this sentence and return JSON only: "Core AI makes provider integration easier."',
    },
  ],
  schema: extractSchema,
  schemaName: 'text_analysis',
  schemaDescription: 'Structured text analysis output.',
});

for await (const event of result) {
  if (event.type === 'object-delta') {
    process.stdout.write(event.text);
    continue;
  }

  if (event.type === 'object') {
    console.log('\n\nValidated object update:', event.object);
  }
}

const response = await result.toResponse();
console.log('\nFinal object:', response.object);
console.log('Finish reason:', response.finishReason);

Object Stream Events

The streamObject() function emits these event types:
type ObjectStreamEvent<TSchema extends z.ZodType> =
  | { type: 'object-delta'; text: string }      // Raw JSON text chunks
  | { type: 'object'; object: z.infer<TSchema> } // Validated partial object
  | { type: 'finish'; finishReason: FinishReason; usage: ChatUsage };
The object event is emitted whenever the partial JSON is valid according to your schema. This may happen multiple times as the object is streamed.

Complex Schema Examples

import { generateObject } from '@core-ai/core-ai';
import { z } from 'zod';

const userProfileSchema = z.object({
  name: z.string(),
  age: z.number(),
  address: z.object({
    street: z.string(),
    city: z.string(),
    country: z.string(),
  }),
  skills: z.array(z.string()),
  metadata: z.record(z.string(), z.unknown()),
});

const result = await generateObject({
  model,
  messages: [{
    role: 'user',
    content: 'Create a sample user profile for a software engineer in Berlin.',
  }],
  schema: userProfileSchema,
});

console.log(result.object.name);
console.log(result.object.address.city);
console.log(result.object.skills);

Schema Descriptions

Add descriptions to help the model understand your schema:
const productSchema = z.object({
  name: z.string().describe('The product name'),
  price: z.number().describe('Price in USD'),
  category: z.enum(['electronics', 'clothing', 'food'])
    .describe('Product category'),
  inStock: z.boolean().describe('Whether the product is currently available'),
  tags: z.array(z.string()).describe('Relevant tags for searching'),
});

const result = await generateObject({
  model,
  messages: [{
    role: 'user',
    content: 'Create a product listing for a wireless keyboard.',
  }],
  schema: productSchema,
  schemaName: 'product',
  schemaDescription: 'A product listing with name, price, and metadata.',
});

Type Inference

TypeScript automatically infers types from Zod schemas:
const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest']),
});

// Type is automatically inferred!
type User = z.infer<typeof userSchema>;
// { id: number; name: string; email: string; role: 'admin' | 'user' | 'guest' }

const result = await generateObject({
  model,
  messages: [{ role: 'user', content: 'Create a user' }],
  schema: userSchema,
});

// result.object is typed as User
const user: User = result.object;

Error Handling

Handle validation and parsing errors:
import {
  StructuredOutputError,
  StructuredOutputValidationError,
  StructuredOutputParseError,
} from '@core-ai/core-ai';

try {
  const result = await generateObject({
    model,
    messages,
    schema: mySchema,
  });

  console.log(result.object);
} catch (error) {
  if (error instanceof StructuredOutputValidationError) {
    console.error('Validation failed:', error.message);
    console.error('Validation errors:', error.issues);
  } else if (error instanceof StructuredOutputParseError) {
    console.error('Failed to parse JSON:', error.message);
    console.error('Raw text:', error.text);
  } else if (error instanceof StructuredOutputError) {
    console.error('Structured output error:', error.message);
  }
}

Streaming with UI Updates

Update your UI as the object is built:
import { streamObject } from '@core-ai/core-ai';
import { z } from 'zod';

const recipeSchema = z.object({
  title: z.string(),
  ingredients: z.array(z.string()),
  instructions: z.array(z.string()),
  prepTime: z.number(),
  cookTime: z.number(),
});

const result = await streamObject({
  model,
  messages: [{ role: 'user', content: 'Give me a pasta recipe' }],
  schema: recipeSchema,
});

for await (const event of result) {
  if (event.type === 'object') {
    // event.object contains the validated partial object
    updateRecipeUI(event.object);
    
    // Check which fields are available
    if (event.object.title) {
      console.log('Title:', event.object.title);
    }
    if (event.object.ingredients && event.object.ingredients.length > 0) {
      console.log('Ingredients so far:', event.object.ingredients.length);
    }
  }
}

const response = await result.toResponse();
console.log('Complete recipe:', response.object);

Configuration Options

Customize structured output generation:
const result = await generateObject({
  model,
  messages,
  schema: mySchema,
  schemaName: 'my_object',           // Name for the schema
  schemaDescription: 'A description',  // Help the model understand
  config: {
    temperature: 0.3,  // Lower temperature for more consistent structure
    maxTokens: 2000,
  },
});

Best Practices

Help the model understand what you want:
const result = await generateObject({
  model,
  messages,
  schema: z.object({
    name: z.string().describe('Full name of the person'),
    age: z.number().describe('Age in years'),
  }),
  schemaName: 'person',
  schemaDescription: 'A person with their basic information',
});
Reduce temperature for more predictable JSON structure:
const result = await generateObject({
  model,
  messages,
  schema: mySchema,
  config: {
    temperature: 0.3, // More deterministic
  },
});
Check which fields are available when streaming:
for await (const event of result) {
  if (event.type === 'object') {
    // Safely check for fields
    if (event.object.field1 !== undefined) {
      console.log('Field 1 is ready:', event.object.field1);
    }
  }
}
Add custom validation after generation:
const result = await generateObject({
  model,
  messages,
  schema: mySchema,
});

// Additional business logic validation
if (result.object.startDate > result.object.endDate) {
  throw new Error('Start date must be before end date');
}

Next Steps

Chat Completion

Generate text responses without structure

Tool Calling

Combine structured outputs with tool calls

Streaming

Stream regular text responses

Zod Documentation

Learn more about Zod schemas

Build docs developers (and LLMs) love