{"slug": "structured-output-is-an-api-evolving-gemini-schemas-without-breaking-your-agent", "title": "Structured Output Is an API: Evolving Gemini Schemas Without Breaking Your Agent", "summary": "A developer outlines a versioning approach for Gemini structured-output schemas, arguing that once a JSON schema is consumed by tools, UIs, agents, or databases it becomes an API that must be evolved additively rather than silently changed. The writeup recommends embedding a schemaVersion discriminator in persisted values, using Zod with the Gemini JavaScript SDK's JSON Schema support, and separating syntax, shape, semantic, and policy validation so that policy violations are blocked rather than retried with reprompts.", "body_md": "Structured output solves an important model problem: instead of asking Gemini for “JSON-like” prose, you provide a schema and receive syntactically structured data.\n\nThen the software-engineering problem begins.\n\nOnce a tool, UI, downstream agent, database, or audit process depends on that JSON, the model output is an API. Renaming a field can break consumers. Adding an enum value can send an older UI into an impossible state. Valid JSON can still describe an invalid business decision.\n\nSuppose a travel assistant initially returns:\n\n```\ntype TravelDecisionV1 = {\n  schemaVersion: \"1\";\n  action: \"book\" | \"ask\" | \"wait\";\n  reason: string;\n};\n```\n\nLater, the product needs suppression and evidence references. Quietly changing the original shape is risky. Introduce a new discriminated version:\n\n```\ntype TravelDecisionV2 = {\n  schemaVersion: \"2\";\n  decision: \"act\" | \"ask\" | \"wait\" | \"suppress\";\n  reasonCode: string;\n  evidenceIds: string[];\n  confidence?: {\n    value: number;\n    source: \"classifier\" | \"model\" | \"rule\";\n  };\n};\n\ntype TravelDecision = TravelDecisionV1 | TravelDecisionV2;\n```\n\nThe version belongs in the persisted value, not only in the deployment configuration. Stored outputs can outlive the model call that created them.\n\nGemini supports structured output through a provided JSON Schema. Its JavaScript SDK can use a JSON Schema together with Zod validation. The abbreviated example below keeps the model identifier in configuration:\n\n``` js\nimport { GoogleGenAI } from \"@google/genai\";\nimport * as z from \"zod\";\n\nconst decisionSchema = z.object({\n  schemaVersion: z.literal(\"2\"),\n  decision: z.enum([\"act\", \"ask\", \"wait\", \"suppress\"]),\n  reasonCode: z.string().min(1),\n  evidenceIds: z.array(z.string()),\n});\n\nconst jsonSchema = z.toJSONSchema(decisionSchema);\nconst client = new GoogleGenAI({});\n\nconst response = await client.interactions.create({\n  model: process.env.GEMINI_MODEL!,\n  input: \"Evaluate the synthetic travel-change fixture.\",\n  response_format: {\n    type: \"text\",\n    mime_type: \"application/json\",\n    schema: jsonSchema,\n  },\n});\n\nconst decision = decisionSchema.parse(\n  JSON.parse(response.output_text),\n);\n```\n\nThe [Gemini structured-output documentation](https://ai.google.dev/gemini-api/docs/structured-output) explicitly recommends application validation. Gemini implements a subset of JSON Schema, and very large or deeply nested schemas may be rejected.\n\nFor a public contract, make the schema as closed and bounded as the supported Gemini subset allows. Limit array sizes, constrain string formats where meaningful, and reject unexpected properties at the application boundary. A permissive schema followed by a strict consumer only moves the failure downstream.\n\n“The response was valid” can mean several different things:\n\n| Check | Example | \n|---|---|\n| Syntax | The response parses as JSON | \n| Shape | Required properties and enum values match the schema | \n| Semantics | Referenced evidence IDs exist | \n| Policy | `act` is permitted for this user and current state | \n\nStructured generation helps primarily with the first two. Your application still owns the last two.\n\n```\nfunction validateSemantics(d: TravelDecisionV2, knownIds: Set<string>) {\n  if (d.decision === \"act\" && d.evidenceIds.length === 0) {\n    throw new Error(\"ACT_REQUIRES_EVIDENCE\");\n  }\n\n  if (d.evidenceIds.some((id) => !knownIds.has(id))) {\n    throw new Error(\"UNKNOWN_EVIDENCE_REFERENCE\");\n  }\n}\n```\n\nDo not retry every validation failure with “please fix your JSON.” A syntax failure, an unsupported enum, and a policy violation require different handling. A policy violation should normally be blocked, not reprompted until the model finds a permissive answer.\n\nChanges are usually safer when they are additive:\n\nEnum additions deserve special care. TypeScript consumers often use exhaustive switches:\n\n```\nfunction renderDecision(d: TravelDecisionV2) {\n  switch (d.decision) {\n    case \"act\": return \"Executing\";\n    case \"ask\": return \"Needs information\";\n    case \"wait\": return \"Monitoring\";\n    case \"suppress\": return \"No action\";\n    default: return \"Unsupported decision\";\n  }\n}\n```\n\nThat fallback protects the UI, but it should also emit a bounded compatibility error.\n\nFor each supported model configuration, test:\n\nDo not assert exact prose in `reasonCode` explanations. Assert the stable schema, references, and business constraints.\n\nPersist a small generation envelope beside the output:\n\n```\ntype GenerationEnvelope = {\n  schemaVersion: \"2\";\n  schemaHash: string;\n  modelId: string;\n  generatedAt: string;\n  value: TravelDecisionV2;\n};\n```\n\n`schemaHash` is not a replacement for the human-readable version. It detects drift when two deployments both claim to produce V2 but were built from different schema documents. The model identifier supports diagnosis; it should not become the only migration key.\n\nStructured output makes agent integration substantially cleaner, but it does not remove API design. Version the value, validate it at runtime, distinguish semantic and policy failures, and preserve migration tests.\n\nIf another component can break when the model's JSON changes, you already have an API—even if there is no HTTP endpoint.", "url": "https://wpnews.pro/news/structured-output-is-an-api-evolving-gemini-schemas-without-breaking-your-agent", "canonical_source": "https://dev.to/raju_dandigam/structured-output-is-an-api-evolving-gemini-schemas-without-breaking-your-agent-44c6", "published_at": "2026-09-17 16:58:46+00:00", "updated_at": "2026-09-17 17:23:20.553056+00:00", "lang": "en", "topics": ["structured-data", "ai-agents", "developer-tools", "large-language-models", "ai-tools"], "entities": ["Gemini", "Google", "Zod", "GoogleGenAI", "TypeScript"], "alternates": {"html": "https://wpnews.pro/news/structured-output-is-an-api-evolving-gemini-schemas-without-breaking-your-agent", "markdown": "https://wpnews.pro/news/structured-output-is-an-api-evolving-gemini-schemas-without-breaking-your-agent.md", "text": "https://wpnews.pro/news/structured-output-is-an-api-evolving-gemini-schemas-without-breaking-your-agent.txt", "jsonld": "https://wpnews.pro/news/structured-output-is-an-api-evolving-gemini-schemas-without-breaking-your-agent.jsonld"}}