description instead of enforced with pattern. The schema looks correct, the output is structurally valid, and then something downstream breaks because a “summary” came back as 3,000 characters or a “status” came back as "In Progress (Pending Review)".
The fix is always the same: encode what you already know. If status has four valid values, use an enum. If a summary feeds into a 240-character column, set maxLength: 240. If a date must be ISO 8601, use format: "date". The model works within whatever constraints you give it; the question is whether you give it enough.
Use enums for known values
Ifstatus is always one of four workflow states, say so. A bare "type": "string" lets the model return "open", "Open", "OPEN", "currently open", or anything else.
Before:
Distinguish optional from nullable
“Not provided” and “explicitly empty” are different states. If your code treats them the same, you can’t tell whether a field was never captured or was captured and found to be missing, and that ambiguity cascades into storage, analytics, and every system that touches the data. Userequired to control presence and "type": ["string", "null"] to allow explicit null values. A field that is both not required and nullable can be absent (not applicable), present with a value, or present as null (asked but unknown):
Add discriminators to unions
Without a discriminator, your code has to guess which branch the model chose by looking at which fields are present. This works until two branches share a field name, and then it doesn’t. Aconst discriminator makes the branch explicit: your runtime reads one field and knows exactly what it’s dealing with.
Before:
Encode field dependencies
Requiring acard_number when the payment method is PayPal is noise. Omitting a card_number when it’s card payment is a bug. If some fields only make sense together, or only make sense for certain values of another field, say so in the schema rather than leaving it to the model’s judgment.
Before:
Compose independent rules with allOf
When you have multiple independent conditions (one based on channel, another based on priority), flatten them into a single if/then and the logic gets unreadable fast. allOf lets you keep each rule as a separate block that’s easy to read, test, and extend independently.
Before:
allOf is self-contained. Adding a new condition means adding a new block, not touching the existing ones.
Bound your strings and arrays
An unbounded"type": "string" can return anything from one character to an essay. An unbounded "type": "array" can return zero items or two hundred. In both cases, the model produces reasonable output most of the time, and then once in a while it doesn’t, and something downstream breaks. Set bounds based on what your system actually accepts.
Before:
maxLength: 240 is better than no limit, even if the real answer turns out to be 280.
Use constraints, not descriptions
If a field must be a valid email address,"description": "Must be a valid business email" is a suggestion the model might follow. "pattern": "^[^@]+@[^@]+$" is a constraint it cannot violate. Descriptions don’t guarantee your output’s format; constraints ensure the model will follow the schema’s syntax.
Before:
format: "date", not "description": "ISO 8601 date". Country codes should be pattern: "^[A-Z]{2}$", not "description": "Two-letter country code". Anything you can express as a constraint, express as a constraint.