cd /news/structured-data/structured-output-is-an-api-evolving… · home topics structured-data article
[ARTICLE · art-132829] src=dev.to ↗ pub= topic=structured-data verified=true sentiment=· neutral

Structured Output Is an API: Evolving Gemini Schemas Without Breaking Your Agent

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.

by read3 min views2 publishedSep 17, 2026

Structured output solves an important model problem: instead of asking Gemini for “JSON-like” prose, you provide a schema and receive syntactically structured data.

Then the software-engineering problem begins.

Once 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.

Suppose a travel assistant initially returns:

type TravelDecisionV1 = {
  schemaVersion: "1";
  action: "book" | "ask" | "wait";
  reason: string;
};

Later, the product needs suppression and evidence references. Quietly changing the original shape is risky. Introduce a new discriminated version:

type TravelDecisionV2 = {
  schemaVersion: "2";
  decision: "act" | "ask" | "wait" | "suppress";
  reasonCode: string;
  evidenceIds: string[];
  confidence?: {
    value: number;
    source: "classifier" | "model" | "rule";
  };
};

type TravelDecision = TravelDecisionV1 | TravelDecisionV2;

The version belongs in the persisted value, not only in the deployment configuration. Stored outputs can outlive the model call that created them.

Gemini 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:

import { GoogleGenAI } from "@google/genai";
import * as z from "zod";

const decisionSchema = z.object({
  schemaVersion: z.literal("2"),
  decision: z.enum(["act", "ask", "wait", "suppress"]),
  reasonCode: z.string().min(1),
  evidenceIds: z.array(z.string()),
});

const jsonSchema = z.toJSONSchema(decisionSchema);
const client = new GoogleGenAI({});

const response = await client.interactions.create({
  model: process.env.GEMINI_MODEL!,
  input: "Evaluate the synthetic travel-change fixture.",
  response_format: {
    type: "text",
    mime_type: "application/json",
    schema: jsonSchema,
  },
});

const decision = decisionSchema.parse(
  JSON.parse(response.output_text),
);

The Gemini structured-output documentation explicitly recommends application validation. Gemini implements a subset of JSON Schema, and very large or deeply nested schemas may be rejected.

For 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.

“The response was valid” can mean several different things:

Check Example
Syntax The response parses as JSON
Shape Required properties and enum values match the schema
Semantics Referenced evidence IDs exist
Policy act is permitted for this user and current state

Structured generation helps primarily with the first two. Your application still owns the last two.

function validateSemantics(d: TravelDecisionV2, knownIds: Set<string>) {
  if (d.decision === "act" && d.evidenceIds.length === 0) {
    throw new Error("ACT_REQUIRES_EVIDENCE");
  }

  if (d.evidenceIds.some((id) => !knownIds.has(id))) {
    throw new Error("UNKNOWN_EVIDENCE_REFERENCE");
  }
}

Do 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.

Changes are usually safer when they are additive:

Enum additions deserve special care. TypeScript consumers often use exhaustive switches:

function renderDecision(d: TravelDecisionV2) {
  switch (d.decision) {
    case "act": return "Executing";
    case "ask": return "Needs information";
    case "wait": return "Monitoring";
    case "suppress": return "No action";
    default: return "Unsupported decision";
  }
}

That fallback protects the UI, but it should also emit a bounded compatibility error.

For each supported model configuration, test:

Do not assert exact prose in reasonCode explanations. Assert the stable schema, references, and business constraints.

Persist a small generation envelope beside the output:

type GenerationEnvelope = {
  schemaVersion: "2";
  schemaHash: string;
  modelId: string;
  generatedAt: string;
  value: TravelDecisionV2;
};

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.

Structured 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.

If another component can break when the model's JSON changes, you already have an API—even if there is no HTTP endpoint.

── more in #structured-data 4 stories · sorted by recency
── more on @gemini 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/structured-output-is…] indexed:0 read:3min 2026-09-17 ·