Search IOCombats

Search challenges, guides, questions and articles

How do you design a JSON schema that a model fills reliably?

Intermediate9 min interview
Skills tested:
Naming fields so their meaning and units are obviousUsing enums, nullable fields and explicit not-found valuesKeeping schemas shallow and splitting very large onesOrdering fields so context is generated before conclusionsKnowing provider limits on schema features and complexity

Advertisement

🧩 Scenario

Your invoice extraction feature fills a schema with keys like amt, dt and type. It works in demos, but on real PDFs it often puts the GST amount in amt, writes dates in three formats and invents a vendor name when there is none. The interviewer asks how you would redesign the schema.

Concept Walkthrough

The schema is part of the prompt

A schema is not only a validator. The model reads key names, types and descriptions, and they steer what it writes. A vague schema is like a vague Jira ticket: you get something back, but not what you meant.

1. Names and units that explain themselves

amt could be the total, the subtotal or the tax. totalAmountInr cannot. Put the unit and format in the name or the description: invoiceDate with "ISO date, YYYY-MM-DD". In Zod, .describe() puts that text into the JSON Schema the model sees.

2. Enums for known values

If type can only be "tax invoice", "credit note" or "receipt", use z.enum([...]). The model picks from a list instead of inventing spellings, and your switch in React stays exhaustive.

3. Give the model a way to say "not there"

If every field is required and there is no vendor on the page, the model has to write something, so it guesses. Make it z.string().nullable() and say in the description "null if not printed on the invoice". For a whole-document miss, add a flag like isInvoice: boolean first. This also fits strict schemas, which prefer nullable fields over optional ones.

4. Context before conclusions

Models write fields in order. A ticket classifier that writes summary before category and priority has already "thought" about the ticket when it picks a category. OpenAI says Structured Outputs keeps your property order. Anthropic notes required properties come first and optional ones after, so check the provider before relying on order.

5. Shallow and small

Every level of nesting and every union adds room for error. Flatten where you can. Providers also have limits: Anthropic documents a 400 error when a schema is "too complex for compilation", and limits on optional and union-type parameters across strict schemas. If a schema keeps growing, split it into two calls.

6. Keep rules the provider cannot enforce in Zod

Numeric ranges, string lengths and regex may be dropped or limited by the provider. Keep them in your Zod schema, describe them in words, and check them after the response.

7. Measure, do not guess

Keep a handful of real inputs (including messy and empty ones) and compare schema versions on them. OpenAI's own tip is to build evals to find the structure that works best.


Key Code Explained

import { z } from 'zod';

// Before: works in demos, guesses on real data
const vague = z.object({
  amt: z.number(),
  dt: z.string(),
  type: z.string(),
  vendor: z.string(),
});

// After: names, units, enums and an honest way to say "missing"
export const invoiceSchema = z.object({
  isInvoice: z
    .boolean()
    .describe('false if the document is not an invoice; then other fields are null'),
  documentType: z
    .enum(['tax_invoice', 'credit_note', 'receipt'])
    .nullable(),
  vendorName: z
    .string()
    .nullable()
    .describe('Exactly as printed. null if no vendor name is printed'),
  invoiceDate: z
    .string()
    .regex(/^\d{4}-\d{2}-\d{2}$/) // checked by Zod after the response
    .nullable()
    .describe('ISO date YYYY-MM-DD. null if not printed'),
  subtotalInr: z.number().nullable().describe('Amount before GST, in rupees'),
  gstInr: z.number().nullable().describe('Total GST amount, in rupees'),
  totalAmountInr: z.number().nullable().describe('Final payable amount, in rupees'),
});

export type Invoice = z.infer<typeof invoiceSchema>;

Notice that three money fields replace one amt. Splitting them removes the ambiguity that caused the GST bug, and lets you add a simple check in code: subtotal plus GST should be close to the total.


Tradeoffs

ChoiceGainCost
Long descriptive namesFewer wrong-field mistakesSlightly more tokens per request
EnumsFixed values, exhaustive UI handlingNew values need a schema change
Nullable plus not-found flagFewer invented valuesUI must handle null everywhere
One big schemaOne callMore errors, may hit provider limits
Split into two callsSimpler schemas, easier to debugMore latency and cost

What Interviewers Actually Check

  • Whether you see the schema as part of the prompt
  • Whether you give the model a valid way to say "unknown"
  • Whether you use enums and units instead of free text
  • Whether you know some constraints are not enforced by the provider
  • Whether you test schema changes on real inputs

Follow-Up Questions

  1. How would you handle an invoice with several GST rates?
  2. When would you add a short explanation field, and where would you put it?
  3. How do you version a schema when old saved data uses the previous shape?
  4. What do you do when a new document type appears that the enum does not cover?
  5. How would you check that subtotal plus GST matches the total?

Common Candidate Mistakes

  • Treating the schema as a type definition only, not as instructions
  • Making every field required with no null option
  • Using one generic amount for several different numbers
  • Packing the whole page into one deeply nested schema
  • Changing the schema without testing on real data

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you rewrite a vague schema with clear names, units and descriptions?
  • Can you explain why a nullable field reduces made-up values?
  • Can you explain how field order can change the output?
  • Can you name constraints you must check in Zod yourself?
  • Can you describe a small test set for comparing schemas?

Summary

A schema is part of the prompt, so design it like a form for a new teammate. Use clear names with units, a description on every field, enums for known values and nullable fields or a not-found flag so the model never has to invent data. Keep the schema shallow, put context before conclusions, and keep rules the provider may not enforce in Zod. Then compare schema versions on a few real inputs, because the only reliable way to know a schema works is to measure it.

Frequently Asked Questions

Do field descriptions actually reach the model?

Yes. Zod .describe() adds a description to the generated JSON Schema, and the AI SDK docs recommend it to give the model hints about each property. OpenAI also lists clear key names and descriptions as a tip for structured outputs.

Why does field order matter?

Models generate tokens in order, so earlier fields are written first and can shape later ones. OpenAI says Structured Outputs keeps the order of your schema properties. Anthropic notes that required properties come first, then optional ones.

Can I use minLength, maximum or regex?

Check your provider. Anthropic says its SDK helpers remove constraints like minimum and maxLength from the sent schema and move them into descriptions, and that regex patterns and string formats are limited to a supported list. Keep those rules in Zod and check them after the response.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Namaste Dev
Sponsored

Cohort-style courses to go from tutorials to job-ready.

Explore Courses

Advertisement