From Zero to AI

Lesson 5.3: Processing Responses

Duration: 50 minutes

Learning Objectives

By the end of this lesson, you will be able to:

  • Extract text content from different API response formats
  • Parse structured data (JSON) from AI responses
  • Validate response data using Zod schemas
  • Handle edge cases and malformed responses
  • Create type-safe response processors
  • Build reusable utilities for response handling

Introduction

Getting a response from an AI API is only half the battle. The real work begins when you need to extract useful information from that response. In this lesson, you will learn how to process AI responses reliably, parse structured data, and handle edge cases that could break your application.


Understanding Response Formats

Both OpenAI and Anthropic return responses in specific structures. Let us create a unified way to handle them.

OpenAI Response Structure

// OpenAI returns content as a string
interface OpenAIResponse {
  choices: Array<{
    message: {
      content: string | null;
    };
  }>;
  usage: {
    prompt_tokens: number;
    completion_tokens: number;
  };
}

Anthropic Response Structure

// Anthropic returns content as an array of blocks
interface AnthropicResponse {
  content: Array<
    { type: 'text'; text: string } | { type: 'tool_use'; id: string; name: string; input: object }
  >;
  usage: {
    input_tokens: number;
    output_tokens: number;
  };
}

Creating a Unified Extractor

Create src/response-parser.ts:

import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';

// Unified response type
interface ParsedResponse {
  content: string;
  tokens: {
    input: number;
    output: number;
    total: number;
  };
  raw: unknown;
}

// Extract content from OpenAI response
function parseOpenAIResponse(response: OpenAI.Chat.ChatCompletion): ParsedResponse {
  return {
    content: response.choices[0]?.message?.content || '',
    tokens: {
      input: response.usage?.prompt_tokens || 0,
      output: response.usage?.completion_tokens || 0,
      total: response.usage?.total_tokens || 0,
    },
    raw: response,
  };
}

// Extract content from Anthropic response
function parseAnthropicResponse(response: Anthropic.Message): ParsedResponse {
  let content = '';

  for (const block of response.content) {
    if (block.type === 'text') {
      content += block.text;
    }
  }

  return {
    content,
    tokens: {
      input: response.usage.input_tokens,
      output: response.usage.output_tokens,
      total: response.usage.input_tokens + response.usage.output_tokens,
    },
    raw: response,
  };
}

export { parseOpenAIResponse, parseAnthropicResponse, ParsedResponse };

Usage:

import { parseOpenAIResponse, parseAnthropicResponse } from "./response-parser";

// With OpenAI
const openaiResponse = await openai.chat.completions.create({...});
const parsed1 = parseOpenAIResponse(openaiResponse);
console.log(parsed1.content);

// With Anthropic
const anthropicResponse = await anthropic.messages.create({...});
const parsed2 = parseAnthropicResponse(anthropicResponse);
console.log(parsed2.content);

Parsing JSON Responses

AI models can return structured JSON data, but they might include extra text or formatting. Here is how to extract JSON reliably.

Basic JSON Extraction

function extractJSON<T>(text: string): T | null {
  // Try to parse the whole text first
  try {
    return JSON.parse(text) as T;
  } catch {
    // Not pure JSON, try to find JSON in the text
  }

  // Look for JSON object pattern
  const jsonMatch = text.match(/\{[\s\S]*\}/);
  if (jsonMatch) {
    try {
      return JSON.parse(jsonMatch[0]) as T;
    } catch {
      // Invalid JSON structure
    }
  }

  // Look for JSON array pattern
  const arrayMatch = text.match(/\[[\s\S]*\]/);
  if (arrayMatch) {
    try {
      return JSON.parse(arrayMatch[0]) as T;
    } catch {
      // Invalid JSON structure
    }
  }

  return null;
}

// Usage
const text = `Here is the data you requested:
\`\`\`json
{"name": "John", "age": 30}
\`\`\`
Let me know if you need anything else!`;

interface Person {
  name: string;
  age: number;
}

const person = extractJSON<Person>(text);
console.log(person); // { name: "John", age: 30 }

Robust JSON Extractor

interface JSONExtractionResult<T> {
  success: boolean;
  data: T | null;
  error?: string;
  rawText: string;
}

function extractJSONSafe<T>(text: string): JSONExtractionResult<T> {
  const result: JSONExtractionResult<T> = {
    success: false,
    data: null,
    rawText: text,
  };

  // Clean the text
  let cleaned = text.trim();

  // Remove markdown code blocks
  cleaned = cleaned.replace(/```json\n?/g, '').replace(/```\n?/g, '');
  cleaned = cleaned.trim();

  // Try direct parse
  try {
    result.data = JSON.parse(cleaned) as T;
    result.success = true;
    return result;
  } catch {
    // Continue to extraction methods
  }

  // Try to find JSON object
  const patterns = [
    /\{[\s\S]*\}/, // Standard object
    /\[[\s\S]*\]/, // Array
    /\{[^{}]*\}/, // Simple object (no nesting)
  ];

  for (const pattern of patterns) {
    const match = cleaned.match(pattern);
    if (match) {
      try {
        result.data = JSON.parse(match[0]) as T;
        result.success = true;
        return result;
      } catch {
        continue;
      }
    }
  }

  result.error = 'Could not extract valid JSON from response';
  return result;
}

Validating with Zod

Parsing JSON is not enough. You need to validate that the data has the expected shape. Zod is perfect for this.

Install Zod

npm install zod

Creating Validated Parsers

import { z } from 'zod';

// Define the expected schema
const PersonSchema = z.object({
  name: z.string().min(1),
  age: z.number().int().positive().optional(),
  email: z.string().email().optional(),
  skills: z.array(z.string()).default([]),
});

// Infer the TypeScript type from the schema
type Person = z.infer<typeof PersonSchema>;

// Validated extraction function
function extractAndValidate<T>(
  text: string,
  schema: z.ZodSchema<T>
): { success: true; data: T } | { success: false; error: string } {
  // First, extract JSON
  const jsonResult = extractJSONSafe<unknown>(text);

  if (!jsonResult.success) {
    return {
      success: false,
      error: jsonResult.error || 'JSON extraction failed',
    };
  }

  // Then, validate with Zod
  const validation = schema.safeParse(jsonResult.data);

  if (!validation.success) {
    const issues = validation.error.issues
      .map((i) => `${i.path.join('.')}: ${i.message}`)
      .join('; ');
    return {
      success: false,
      error: `Validation failed: ${issues}`,
    };
  }

  return {
    success: true,
    data: validation.data,
  };
}

// Usage
const aiResponse = `Based on the text, here is the extracted info:
{
  "name": "Alice",
  "age": 25,
  "skills": ["TypeScript", "React"]
}`;

const result = extractAndValidate(aiResponse, PersonSchema);

if (result.success) {
  console.log('Valid person:', result.data);
  // TypeScript knows result.data is Person
} else {
  console.error('Extraction failed:', result.error);
}

Creating Type-Safe Response Handlers

Let us build a complete response handler that works with AI APIs.

import Anthropic from '@anthropic-ai/sdk';
import 'dotenv/config';
import OpenAI from 'openai';
import { z } from 'zod';

// Initialize clients
const openai = new OpenAI();
const anthropic = new Anthropic();

// Generic structured response handler
class StructuredResponseHandler<T> {
  constructor(
    private schema: z.ZodSchema<T>,
    private extractionPrompt: string
  ) {}

  private buildPrompt(input: string): string {
    return `${this.extractionPrompt}

Input:
${input}

Respond with valid JSON only. No explanation, no markdown formatting.`;
  }

  async extractWithOpenAI(input: string): Promise<T> {
    const response = await openai.chat.completions.create({
      model: 'gpt-4o-mini',
      messages: [
        {
          role: 'system',
          content: 'You extract structured data and respond with JSON only.',
        },
        {
          role: 'user',
          content: this.buildPrompt(input),
        },
      ],
      response_format: { type: 'json_object' },
    });

    const content = response.choices[0].message.content || '{}';
    const parsed = JSON.parse(content);
    return this.schema.parse(parsed);
  }

  async extractWithAnthropic(input: string): Promise<T> {
    const response = await anthropic.messages.create({
      model: 'claude-sonnet-4-20250514',
      max_tokens: 1024,
      system: 'You extract structured data and respond with JSON only.',
      messages: [
        {
          role: 'user',
          content: this.buildPrompt(input),
        },
      ],
    });

    const block = response.content[0];
    if (block.type !== 'text') {
      throw new Error('Expected text response');
    }

    const parsed = JSON.parse(block.text);
    return this.schema.parse(parsed);
  }
}

// Example usage: Product Info Extractor
const ProductSchema = z.object({
  name: z.string(),
  price: z.number().positive(),
  currency: z.string().length(3),
  inStock: z.boolean(),
  features: z.array(z.string()),
});

type Product = z.infer<typeof ProductSchema>;

const productExtractor = new StructuredResponseHandler<Product>(
  ProductSchema,
  `Extract product information from the following text.
Return JSON with: name (string), price (number), currency (3-letter code), 
inStock (boolean), features (array of strings).`
);

// Test
async function main() {
  const description = `
The new UltraWidget Pro is now available for $299.99 USD.
Features include wireless charging, water resistance, and 48-hour battery life.
Currently in stock and shipping within 24 hours.
  `;

  const product = await productExtractor.extractWithOpenAI(description);
  console.log('Extracted product:', product);
}

main();

Handling Edge Cases

AI responses can be unpredictable. Here is how to handle common edge cases.

Empty or Missing Content

function safeExtractContent(response: unknown): string {
  // Handle null/undefined
  if (response == null) {
    return '';
  }

  // Handle OpenAI format
  if (typeof response === 'object' && 'choices' in response) {
    const openaiRes = response as OpenAI.Chat.ChatCompletion;
    return openaiRes.choices[0]?.message?.content || '';
  }

  // Handle Anthropic format
  if (typeof response === 'object' && 'content' in response) {
    const anthropicRes = response as Anthropic.Message;
    for (const block of anthropicRes.content) {
      if (block.type === 'text') {
        return block.text;
      }
    }
    return '';
  }

  // Handle string
  if (typeof response === 'string') {
    return response;
  }

  return '';
}

Handling Truncated Responses

interface ResponseWithStatus {
  content: string;
  isComplete: boolean;
  finishReason: string;
}

function parseWithStatus(response: OpenAI.Chat.ChatCompletion): ResponseWithStatus {
  const choice = response.choices[0];

  return {
    content: choice?.message?.content || '',
    isComplete: choice?.finish_reason === 'stop',
    finishReason: choice?.finish_reason || 'unknown',
  };
}

// Usage with handling
async function getCompleteResponse(prompt: string): Promise<string> {
  const response = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    max_tokens: 500,
  });

  const parsed = parseWithStatus(response);

  if (!parsed.isComplete) {
    console.warn(`Response truncated: ${parsed.finishReason}`);
    // Could retry with higher max_tokens or handle differently
  }

  return parsed.content;
}

Handling Malformed JSON with Recovery

function recoverJSON(text: string): unknown {
  // Step 1: Try direct parse
  try {
    return JSON.parse(text);
  } catch {
    // Continue to recovery
  }

  // Step 2: Fix common issues
  let fixed = text;

  // Remove trailing commas before ] or }
  fixed = fixed.replace(/,\s*([}\]])/g, '$1');

  // Add missing quotes to keys
  fixed = fixed.replace(/([{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3');

  // Replace single quotes with double quotes
  fixed = fixed.replace(/'/g, '"');

  try {
    return JSON.parse(fixed);
  } catch {
    // Continue to extraction
  }

  // Step 3: Extract object/array
  const objectMatch = fixed.match(/\{[\s\S]*\}/);
  if (objectMatch) {
    try {
      return JSON.parse(objectMatch[0]);
    } catch {
      // Failed
    }
  }

  // Step 4: Return null if all recovery fails
  return null;
}

// Example of malformed JSON that can be recovered
const malformed = `{
  name: 'John',
  age: 30,
  skills: ['TypeScript', 'React',],
}`;

const recovered = recoverJSON(malformed);
console.log(recovered); // { name: 'John', age: 30, skills: ['TypeScript', 'React'] }

Building a Complete Response Processor

Let us create a comprehensive response processor that handles all cases.

Create src/response-processor.ts:

import { z } from 'zod';

// Types
interface ProcessingResult<T> {
  success: boolean;
  data: T | null;
  rawContent: string;
  errors: string[];
  warnings: string[];
}

interface ProcessorOptions {
  allowPartialJSON?: boolean;
  trimContent?: boolean;
  removeMarkdown?: boolean;
}

// Main processor class
class ResponseProcessor<T> {
  private schema: z.ZodSchema<T>;
  private options: ProcessorOptions;

  constructor(schema: z.ZodSchema<T>, options: ProcessorOptions = {}) {
    this.schema = schema;
    this.options = {
      allowPartialJSON: true,
      trimContent: true,
      removeMarkdown: true,
      ...options,
    };
  }

  process(rawContent: string): ProcessingResult<T> {
    const result: ProcessingResult<T> = {
      success: false,
      data: null,
      rawContent,
      errors: [],
      warnings: [],
    };

    // Step 1: Clean content
    let content = rawContent;

    if (this.options.trimContent) {
      content = content.trim();
    }

    if (this.options.removeMarkdown) {
      content = this.removeMarkdownFormatting(content);
    }

    if (!content) {
      result.errors.push('Empty content after cleaning');
      return result;
    }

    // Step 2: Extract JSON
    const jsonData = this.extractJSON(content);

    if (jsonData === null) {
      result.errors.push('Could not extract JSON from content');
      return result;
    }

    // Step 3: Validate with schema
    const validation = this.schema.safeParse(jsonData);

    if (!validation.success) {
      for (const issue of validation.error.issues) {
        result.errors.push(`Validation error at ${issue.path.join('.')}: ${issue.message}`);
      }
      return result;
    }

    // Success
    result.success = true;
    result.data = validation.data;
    return result;
  }

  private removeMarkdownFormatting(text: string): string {
    return text
      .replace(/```json\n?/gi, '')
      .replace(/```\n?/g, '')
      .replace(/`/g, '')
      .trim();
  }

  private extractJSON(text: string): unknown | null {
    // Try direct parse
    try {
      return JSON.parse(text);
    } catch {
      // Continue
    }

    // Try to find JSON object
    const objectMatch = text.match(/\{[\s\S]*\}/);
    if (objectMatch) {
      try {
        return JSON.parse(objectMatch[0]);
      } catch {
        // Try recovery
        if (this.options.allowPartialJSON) {
          return this.recoverJSON(objectMatch[0]);
        }
      }
    }

    // Try to find JSON array
    const arrayMatch = text.match(/\[[\s\S]*\]/);
    if (arrayMatch) {
      try {
        return JSON.parse(arrayMatch[0]);
      } catch {
        if (this.options.allowPartialJSON) {
          return this.recoverJSON(arrayMatch[0]);
        }
      }
    }

    return null;
  }

  private recoverJSON(text: string): unknown | null {
    let fixed = text;

    // Fix trailing commas
    fixed = fixed.replace(/,\s*([}\]])/g, '$1');

    // Add quotes to unquoted keys
    fixed = fixed.replace(/([{,]\s*)(\w+)(\s*:)/g, '$1"$2"$3');

    // Replace single quotes
    fixed = fixed.replace(/'/g, '"');

    try {
      return JSON.parse(fixed);
    } catch {
      return null;
    }
  }
}

export { ResponseProcessor, ProcessingResult, ProcessorOptions };

Usage:

import { z } from 'zod';

import { ResponseProcessor } from './response-processor';

// Define schema
const TaskSchema = z.object({
  title: z.string().min(1),
  priority: z.enum(['low', 'medium', 'high']),
  dueDate: z.string().optional(),
  tags: z.array(z.string()).default([]),
});

type Task = z.infer<typeof TaskSchema>;

// Create processor
const taskProcessor = new ResponseProcessor<Task>(TaskSchema);

// Process AI response
const aiResponse = `Here's the task I extracted:
\`\`\`json
{
  "title": "Review pull request",
  "priority": "high",
  "dueDate": "2024-03-15",
  "tags": ["code-review", "urgent"]
}
\`\`\``;

const result = taskProcessor.process(aiResponse);

if (result.success) {
  console.log('Task:', result.data);
} else {
  console.error('Errors:', result.errors);
}

Processing Lists and Collections

Often you need to extract multiple items from a response.

import { z } from 'zod';

const ItemSchema = z.object({
  name: z.string(),
  quantity: z.number().int().positive(),
});

const ItemListSchema = z.object({
  items: z.array(ItemSchema),
  total: z.number().int().nonnegative(),
});

type ItemList = z.infer<typeof ItemListSchema>;

async function extractShoppingList(text: string): Promise<ItemList> {
  const response = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      {
        role: 'system',
        content: 'Extract shopping list items. Return JSON with items array and total count.',
      },
      {
        role: 'user',
        content: `Extract items from: "${text}"`,
      },
    ],
    response_format: { type: 'json_object' },
  });

  const content = response.choices[0].message.content || '{}';
  const parsed = JSON.parse(content);

  return ItemListSchema.parse(parsed);
}

// Usage
const list = await extractShoppingList('I need 3 apples, 2 loaves of bread, and a dozen eggs');
console.log(list);
// { items: [{name: "apples", quantity: 3}, ...], total: 3 }

Exercises

Exercise 1: Sentiment Analyzer

Create a response processor for sentiment analysis:

// Your implementation here
// Define a schema for sentiment analysis results
// Create a processor that extracts sentiment from AI responses
// Handle cases where the AI might return invalid data

interface SentimentResult {
  sentiment: 'positive' | 'negative' | 'neutral';
  confidence: number;
  keywords: string[];
}

async function analyzeSentiment(text: string): Promise<SentimentResult> {
  // TODO: Implement
}
Solution
import 'dotenv/config';
import OpenAI from 'openai';
import { z } from 'zod';

const openai = new OpenAI();

const SentimentSchema = z.object({
  sentiment: z.enum(['positive', 'negative', 'neutral']),
  confidence: z.number().min(0).max(1),
  keywords: z.array(z.string()),
});

type SentimentResult = z.infer<typeof SentimentSchema>;

async function analyzeSentiment(text: string): Promise<SentimentResult> {
  const response = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [
      {
        role: 'system',
        content: `Analyze the sentiment of the given text.
Return JSON with:
- sentiment: "positive", "negative", or "neutral"
- confidence: number between 0 and 1
- keywords: array of words that influenced the sentiment`,
      },
      {
        role: 'user',
        content: text,
      },
    ],
    response_format: { type: 'json_object' },
  });

  const content = response.choices[0].message.content || '{}';

  try {
    const parsed = JSON.parse(content);
    return SentimentSchema.parse(parsed);
  } catch (error) {
    // Return neutral with low confidence on parse failure
    return {
      sentiment: 'neutral',
      confidence: 0,
      keywords: [],
    };
  }
}

// Test
async function main() {
  const positive = await analyzeSentiment(
    "This product is absolutely amazing! Best purchase I've ever made."
  );
  console.log('Positive text:', positive);

  const negative = await analyzeSentiment('Terrible experience. The product broke after one day.');
  console.log('Negative text:', negative);
}

main();

Exercise 2: Multi-Format Parser

Create a parser that handles different response formats:

// Your implementation here
// Support JSON, bullet lists, and numbered lists
// Convert all formats to a unified structure

interface ParsedItems {
  format: 'json' | 'bullets' | 'numbered' | 'unknown';
  items: string[];
}

function parseAIList(response: string): ParsedItems {
  // TODO: Implement
}
Solution
interface ParsedItems {
  format: 'json' | 'bullets' | 'numbered' | 'unknown';
  items: string[];
}

function parseAIList(response: string): ParsedItems {
  const text = response.trim();

  // Try JSON format first
  try {
    const json = JSON.parse(text);
    if (Array.isArray(json)) {
      return {
        format: 'json',
        items: json.map(String),
      };
    }
    if (json.items && Array.isArray(json.items)) {
      return {
        format: 'json',
        items: json.items.map(String),
      };
    }
  } catch {
    // Not JSON, continue
  }

  // Check for numbered list (1. item, 2. item, etc.)
  const numberedPattern = /^\d+[\.\)]\s+(.+)$/gm;
  const numberedMatches = [...text.matchAll(numberedPattern)];

  if (numberedMatches.length > 0) {
    return {
      format: 'numbered',
      items: numberedMatches.map((m) => m[1].trim()),
    };
  }

  // Check for bullet list (- item, * item, • item)
  const bulletPattern = /^[\-\*\•]\s+(.+)$/gm;
  const bulletMatches = [...text.matchAll(bulletPattern)];

  if (bulletMatches.length > 0) {
    return {
      format: 'bullets',
      items: bulletMatches.map((m) => m[1].trim()),
    };
  }

  // Check for lines separated by newlines
  const lines = text
    .split('\n')
    .map((line) => line.trim())
    .filter((line) => line.length > 0);

  if (lines.length > 1) {
    return {
      format: 'unknown',
      items: lines,
    };
  }

  // Single item or unparseable
  return {
    format: 'unknown',
    items: text ? [text] : [],
  };
}

// Tests
console.log(parseAIList(`["apple", "banana", "cherry"]`));
// { format: "json", items: ["apple", "banana", "cherry"] }

console.log(
  parseAIList(`1. First item
2. Second item
3. Third item`)
);
// { format: "numbered", items: ["First item", "Second item", "Third item"] }

console.log(
  parseAIList(`- Apple
- Banana
- Cherry`)
);
// { format: "bullets", items: ["Apple", "Banana", "Cherry"] }

Exercise 3: Streaming Response Accumulator

Create a utility to accumulate streaming responses:

// Your implementation here
// Accumulate chunks from a streaming response
// Provide methods to get current content and final result

class StreamAccumulator {
  private chunks: string[] = [];

  addChunk(chunk: string): void {
    // TODO: Add chunk
  }

  getCurrentContent(): string {
    // TODO: Return accumulated content
  }

  async processWithSchema<T>(schema: z.ZodSchema<T>): Promise<T | null> {
    // TODO: Try to parse accumulated content with schema
  }
}
Solution
import { z } from 'zod';

class StreamAccumulator {
  private chunks: string[] = [];

  addChunk(chunk: string): void {
    this.chunks.push(chunk);
  }

  getCurrentContent(): string {
    return this.chunks.join('');
  }

  getChunkCount(): number {
    return this.chunks.length;
  }

  clear(): void {
    this.chunks = [];
  }

  async processWithSchema<T>(schema: z.ZodSchema<T>): Promise<T | null> {
    const content = this.getCurrentContent().trim();

    if (!content) {
      return null;
    }

    // Try to extract and parse JSON
    try {
      // Remove markdown if present
      let cleaned = content
        .replace(/```json\n?/gi, '')
        .replace(/```\n?/g, '')
        .trim();

      // Try direct parse
      const parsed = JSON.parse(cleaned);
      const validated = schema.safeParse(parsed);

      if (validated.success) {
        return validated.data;
      }
    } catch {
      // Try to find JSON in content
      const jsonMatch = content.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
      if (jsonMatch) {
        try {
          const parsed = JSON.parse(jsonMatch[0]);
          const validated = schema.safeParse(parsed);

          if (validated.success) {
            return validated.data;
          }
        } catch {
          // Failed
        }
      }
    }

    return null;
  }
}

// Test
const accumulator = new StreamAccumulator();

// Simulate streaming chunks
const chunks = ['{"name": ', '"John", ', '"age": 30, ', '"skills": ["TS", "JS"]}'];

for (const chunk of chunks) {
  accumulator.addChunk(chunk);
  console.log(`After chunk ${accumulator.getChunkCount()}:`, accumulator.getCurrentContent());
}

// Parse final result
const PersonSchema = z.object({
  name: z.string(),
  age: z.number(),
  skills: z.array(z.string()),
});

accumulator.processWithSchema(PersonSchema).then((result) => {
  console.log('Final parsed result:', result);
});

Key Takeaways

  1. Unified Parsing: Create functions that handle both OpenAI and Anthropic formats
  2. JSON Extraction: Always try multiple extraction methods for robustness
  3. Zod Validation: Use schemas to ensure type safety and data integrity
  4. Error Recovery: Implement JSON recovery for malformed responses
  5. Edge Cases: Handle empty content, truncation, and missing fields
  6. Type Safety: Use generics and Zod inference for full type coverage
  7. Reusable Processors: Build classes that can be reused across your application

Resources

Resource Type Description
Zod Documentation Documentation Complete Zod reference
TypeScript Utility Types Documentation Built-in TypeScript types
JSON.parse MDN Documentation JSON parsing reference
OpenAI JSON Mode Documentation OpenAI structured output guide

Next Lesson

You have learned how to process AI responses reliably. In the next lesson, you will learn how to implement rate limiting and retry logic to make your API integrations production-ready.

Continue to Lesson 5.4: Rate Limiting and Retry Logic