Structured Outputs in AI: How to Define, Validate, and Deploy a JSON Contract
Imagen generada con gpt-image-2.5-sunburst para InferamaSource ↗
01

Structured output is a contract, not a guarantee of truth

A structured output is an AI system response presented according to an agreed data shape—for example, a JSON object with fields, types, and permitted values. In an application, that contract lets you parse the result and pass it to downstream code without extracting data from a free-form paragraph. It does not, by itself, make the content true, complete, or suitable for triggering an action.

The distinction matters. The text “the request is for Tuesday” might be correctly transformed into a string containing a date, but the application still needs to know which Tuesday is meant, whether the person provided that date, and whether the result is consistent with the rest of the request. Structure defines how a response is represented. Data correctness also depends on the input, the task, and the business rules.

It is therefore useful to think in terms of three separate checks: whether the response can be read as JSON; whether it satisfies the agreed schema; and whether its values make sense and are supported by the available information. A response can pass the first check and fail the other two. It can also satisfy a schema and still be an incorrect extraction.

This guide uses a fictional vacation-request extraction example to illustrate the design. The example is a proposed local implementation, not a claim about any API’s required format. Check the current documentation for the selected service to confirm specific provider capabilities, parameter names, supported models, and error states.

02

Decide whether you need structured output

Impose structured output when the result will feed an interface, search, work queue, automated rule, or database, and when the expected set of fields can be described stably. For example, a request-management application might need a status, a request type, and consistently identified dates. In that case, asking for free-form text and extracting fields afterward adds another interpretation step and more points of failure.

It is not always necessary. If someone only wants an explanation, a conversational answer may be clearer than a data object. If a deterministic transformation is enough—such as normalizing whitespace or converting a date that already has an unambiguous format—a conventional function may be simpler and more reproducible than asking a model to do it. And if the problem does not yet have defined fields, formalizing a schema too early may conceal the fact that the task itself is poorly framed.

The decision should also account for the cost of an error. If a response triggers payments, changes permissions, deletes information, or affects a sensitive decision, the schema is only one control. The application may also need human confirmation, additional permissions, or a rule that prevents the action until an ambiguity is resolved. Do not delegate those decisions to presumed format safety.

Before designing the contract, write down who consumes each field and what they use it for. If no component uses a field, it may not belong in the output. If a component requires precision that the input cannot guarantee, define an uncertainty state or a clarification question instead of forcing the model to invent a value.

An initial decision

NeedInitial optionCaution
An answer intended for a person, with no downstream processingFree-form textA rigid structure may make it harder to read without adding value.
Predictable fields that code will consumeA data contract and local validationThe format does not prove that the values are correct.
An exact, deterministic transformationConventional codeDo not add generation if a small function solves the problem.
Ambiguous or insufficient inputA clarification or review stateDo not force made-up values to fill fields.
03

Define a contract that represents the task

A useful contract describes the shape of each field and the conditions for using it. Start by identifying the minimum fields, assigning types, and deciding which are required. Then specify permitted values, how missing data is represented, and relationships between fields. Finally, keep these rules separate from the instructions sent to the model: the application should retain a verifiable definition that can be tested without depending on what the model appears to have understood.

For a vacation-request extraction, an overly simple contract might always require a start date and an end date. But a request such as “I’d like to take a few days off next month” does not provide enough information to determine exact dates. Requiring those dates pushes the system toward guessing. One alternative is to make task states explicit: request complete, clarification needed, or text that is not a request. Dates can be empty when unknown, and a question can explain what information is missing.

The following schema illustrates one possible design. The condition that the end date must not precede the start date, and the relationship between the status and optional fields, must also be checked in the application. Do not assume that a generation mode or validator supports every keyword shown here: the local schema and the format accepted by an API may not be interchangeable. If the remote interface supports only a subset, adapt the remote representation and retain the complete checks in your code.

The convention for missing data must be unambiguous. In this example, an unknown date is represented as null, not as an empty string, “unknown,” or an invented date. A single consistent value makes validation easier and prevents different consumers from interpreting the same case differently. The application must also decide whether to accept or reject extra fields; allowing them without review can conceal version errors or unexpected output.

Illustrative local schema · json

This schema documents a proposed contract. It does not imply direct compatibility with any provider’s generation interface.

{
  "type": "object",
  "additionalProperties": false,
  "required": ["status", "start_date", "end_date", "clarification_question"],
  "properties": {
    "status": {
      "type": "string",
      "enum": ["complete", "needs_clarification", "not_a_request"]
    },
    "start_date": {
      "type": ["string", "null"],
      "description": "ISO date YYYY-MM-DD, only when supported by the request"
    },
    "end_date": {
      "type": ["string", "null"],
      "description": "ISO date YYYY-MM-DD, only when supported by the request"
    },
    "clarification_question": {
      "type": ["string", "null"]
    }
  }
}
04

Separate format, schema, and meaning

The first validation asks whether the received bytes can be interpreted as JSON. If the response contains a trailing comma, text added outside the object, or an unclosed quotation mark, parsing may fail. The second asks whether the parsed value has the permitted shape: an object, the expected keys, and values of appropriate types. The third asks whether the information correctly represents the request and follows the application’s rules.

For example, this object could be valid JSON and satisfy the contract’s basic types: status says the request is complete, both dates are strings, and there is no clarification question. Even so, if the person requested vacation “from June 12 to June 10,” the response is inconsistent. Likewise, the range could be internally valid but unsupported by the text. Type checks do not replace checking whether the response corresponds to the input.

Write semantic rules as explicit conditions and, where possible, as reproducible tests. For this example: if status is complete, both dates must exist and be valid calendar dates; the end date cannot precede the start date; if status is needs_clarification, the response must state what information is missing; and if status is not_a_request, the vacation workflow must not start. The precise policy can vary, but it should not be left implicit in the consumer’s interpretation.

Also distinguish an invalid response from a valid response indicating that the task cannot be completed. needs_clarification can be a correct response within the contract, not a technical error. If the API reports a refusal, generation error, or interruption in a provider-specific form, the application should handle that state as documented by the provider. Do not turn all of these situations into an apparently normal object without knowing what happened.

05

Choose a mode without assuming compatibility

Some APIs offer specific options for requesting structured outputs, but names, requirements, and limits belong to individual products and can change. Amazon Nova’s official documentation covers structured output requirements and formats such as JSON. Anthropic’s documentation describes structured outputs with a JSON schema and the output_config parameter. Google documents structured output for the Gemini API and mentions JSON schemas. OpenAI presents its structured outputs functionality for the API. These are references for specific products, not evidence that the products share the same contract, level of constraint, or way of handling failures.

Microsoft Learn also has a page on structured outputs for chat models in documentation identified as Foundry classic. That scope matters: a documentation path associated with one product or mode should not be treated as a universal specification for all Microsoft services. Similarly, documentation for Claude Code and Claude Platform refers to different products; an option in one should not automatically be assumed to apply to the other.

Before choosing, check the current primary documentation for the available mode, the models or versions it covers, which parts of a schema it accepts, what happens with optional fields and enumerations, how refusals or incomplete generations are represented, and what response structure the application receives. Record the date and version of the documentation you consulted. If a requirement in your contract is not specified, treat it as an uncertainty and verify it with tests in the relevant environment, not as a guaranteed capability.

Do not confuse requesting JSON with requiring schema compliance. A mode that produces syntactically valid JSON may reduce parsing work, but the application still needs to check keys, types, and task rules. Even when documentation describes schema constraints for a mode, keep local validation: the data contract that protects the consuming system must be checked at the application’s trust boundary.

Compatibility checks before implementation

  1. 01Identify the exact API, product, version, and model you plan to use.
  2. 02Consult current official documentation for the structured-output mode and its documented constraints.
  3. 03Compare the fields, types, optionality, enumerations, and nested rules in your contract with what the interface supports.
  4. 04Find out how refusals, errors, interruptions, and responses that do not complete the requested format are described.
  5. 05Test the integration in the selected environment and retain local validation, even if the request includes format constraints.
06

Implement validation in the application

The application should parse the response at a controlled point and validate it before handing it to the next component. A practical sequence is: receive the API result; first check the documented transport and generation states; extract the relevant content; parse JSON; verify that the root is an object with permitted keys; validate types and values; and finally apply business rules. If any stage fails, the flow should retain enough information to diagnose the problem without acting as though the result were correct.

The example below is executable Python code using the standard library. It expects a JSON string, permits exactly four keys, checks types, parses dates in ISO format, and applies relationships between status, dates, and the question. It does not validate that a date actually appeared in the input text: that requires retaining the text, defining an evidence-verification strategy, and testing it separately. Nor does it replace API error handling, which must be adapted to the service in use.

The example deliberately rejects extra fields. That policy helps prevent an unexpected change from going unnoticed, although a system with controlled evolution may choose another rule. What matters is making the choice explicit and versioning the contract. If you use an external validation library in production, pin a compatible version, test its rules, and treat validator errors as part of your internal interface—not as a reason to bypass the check.

Do not silently turn an error into a default value. If end_date is missing, filling it with start_date may change the meaning of the request. If a date is invalid, treating it as null may erase a useful failure signal. It is preferable to return a typed validation result—valid, clarification required, or invalid—and make an explicit decision about whether to retry, ask the person, or send the case for review.

Local validation with standard Python · python

Save the code as validate_leave.py and run it with Python 3. The example prints the data only after validating the contract and its basic relationships.

import json
from datetime import date

REQUIRED = {
    "status",
    "start_date",
    "end_date",
    "clarification_question",
}
STATUSES = {"complete", "needs_clarification", "not_a_request"}


def validate(raw):
    try:
        data = json.loads(raw)
    except json.JSONDecodeError as exc:
        raise ValueError("Invalid JSON") from exc

    if not isinstance(data, dict) or set(data) != REQUIRED:
        raise ValueError("Object has missing or unexpected keys")
    if data["status"] not in STATUSES:
        raise ValueError("Disallowed status")

    for key in ("start_date", "end_date", "clarification_question"):
        value = data[key]
        if value is not None and not isinstance(value, str):
            raise ValueError(f"Incorrect type: {key}")

    dates = []
    for key in ("start_date", "end_date"):
        value = data[key]
        if value is None:
            dates.append(None)
            continue
        parsed = date.fromisoformat(value)
        if parsed.isoformat() != value:
            raise ValueError(f"Incorrect date format: {key}")
        dates.append(parsed)

    start, end = dates
    if data["status"] == "complete":
        if start is None or end is None or end < start:
            raise ValueError("Complete interval is missing or inconsistent")
        if data["clarification_question"] is not None:
            raise ValueError("A complete request must not ask for clarification")
    elif data["status"] == "needs_clarification":
        if not data["clarification_question"]:
            raise ValueError("Specify what information needs clarification")
    elif data["status"] == "not_a_request":
        if start is not None or end is not None:
            raise ValueError("Non-request text must not include dates")

    return data


if __name__ == "__main__":
    sample = '{"status":"complete","start_date":"2027-06-12","end_date":"2027-06-14","clarification_question":null}'
    print(validate(sample))
07

Define what to do for each failure

A useful failure policy distinguishes at least four situations. First, transport or service errors: no usable response was obtained. Second, generation errors or refusals: the API may report that it did not produce the requested result. Third, format or schema errors: the content cannot be parsed or does not satisfy the contract. Fourth, semantic errors: the shape passes validation, but the content is incomplete, contradictory, or unsupported.

Each category needs an explicit outcome. A documented temporary error may allow a limited retry; repeating an ambiguous request does not create information that was absent from the input. A schema failure may justify another attempt with clearer instructions, but only if the number of attempts is bounded and the result is validated again. Semantically inconsistent data may require a question to the person or human review. A refusal should be surfaced or handled according to the product policy and the details documented by the provider.

Set limits to prevent retry loops, and avoid automatically repairing values in ways that change meaning. If a retry produces a different output, do not silently choose the one that seems more convenient. Keep the validation state, contract version, and categorized failure reason. For consequential operations, consider a confirmation stage before carrying out the real-world effect.

Technical logs should support diagnosis with the least possible exposure of data. Store correlation identifiers, contract version, error category, and necessary metrics. Assess whether retaining the full input text or output is necessary; if it is not, do not log it by default. If it is retained, define appropriate access, retention period, and handling for the sensitivity of that data.

Response by failure category

SituationPrudent responseAvoid
Transport or service failureApply the documented retry policy, with a limit and logging.Treat a missing response as a valid empty extraction.
Malformed JSON or schema violationMark the output invalid and decide whether to attempt a bounded correction.Remove unexpected parts until the result looks acceptable.
Required field missing or input ambiguousRequest the missing information or route the case for review.Invent a default value that changes the request.
Contradictory or unsupported valuesBlock the downstream action and retain the failure category.Assume valid format means correct content.
08

Test real inputs and edge cases

Tests should verify behavior, not just that an example response can be parsed. Prepare a small, maintainable set of representative inputs, expected results, and rules that must hold. Include ordinary, ambiguous, incomplete, contradictory, and out-of-scope cases. Each case should test both the output considered acceptable and the application’s decision after receiving it.

For the vacation request, a clear input might request complete dates in a consistent range. An ambiguous case might say “on Friday” without enough temporal context. Another might request dates but omit the year. An incomplete case might omit the end date. Also test a reversed range, an impossible date, an incorrect type, an extra key, and a response that is not JSON. For each one, decide whether the application accepts, asks for clarification, or rejects; do not leave that decision to ad hoc interpretation during integration.

Include semantic tests with prepared outputs as well as tests with the model. If you test only live generations, results may vary, making it difficult to isolate whether generation, the contract, or consumer code failed. Deterministic validator tests let you check exact cases; integration tests observe how the complete flow behaves with the API and configuration that will actually be deployed.

When changing the schema, add tests for compatibility between versions. If a consumer expects a field that is no longer available, a seemingly small change can break processing. Version the contract and decide how producer and consumer will be deployed—for example, by allowing a transition period with both versions or coordinating the change. Do not present a change to names or nullability as cosmetic.

Minimum test matrix

CaseCheckExpected application result
Clear request with consistent datesTypes, format, interval, and correspondence with the input.Accept as complete if all rules pass.
Relative or ambiguous dateDo not turn an incomplete reference into a certain date without support.Ask for clarification or mark uncertainty.
Missing dateCheck the null convention and its relationship to status.Do not automatically fill it with another date.
Impossible date or reversed rangeValidate calendar date and chronological order.Reject as semantically invalid output.
Extra key, incorrect type, or malformed JSONTest parsing and contract boundaries.Block processing and classify the error.
Text that is not a requestCheck the not_a_request state and ensure there are no action dates.Do not start the vacation workflow.
09

Deploy and maintain the contract

A deployed contract needs an owner, a version, and a change process. Keep the definition alongside the code that validates it, and record which version was used for each processed result. Before changing fields, types, enumerations, or missing-value rules, assess the consumers and add specific regression tests. If the model, prompt, configuration, or API changes, rerun the relevant tests: the contract does not replace verification of the complete system’s behavior.

Measure separately the proportion of responses that can be parsed, that satisfy the schema, that pass semantic rules, and that require clarification or review. A single “success” indicator conceals important differences. For example, having almost every response be valid JSON does not tell you whether the extracted dates match the requests. Metrics should have stable definitions and avoid collecting sensitive content when an aggregate category is sufficient.

Monitor provider documentation and version changes, but do not assume an update preserves exactly the capabilities of the previous configuration. Record the combination that was tested and verify compatibility conditions before changing it. A service’s documentation page is a reference for the service and scope it describes; it does not certify identical behavior in another product, version, or model.

If the system makes consequential decisions, add a safeguard proportionate to the risk. This could be human review, explicit confirmation, or an independent check. Design a recovery path too: what to do with pending results, how to correct a datum that was validated incorrectly, and how to avoid executing an action twice after a retry. A data contract is part of an operational workflow, not a replacement for its controls.

Acceptance checklist before connecting a downstream action

  1. 01Each field has a consumer, a type, and an explicit policy for missing values.
  2. 02Conditions between fields and business rules are written down and tested locally.
  3. 03The system distinguishes service errors, format errors, clarification states, and semantic inconsistencies.
  4. 04The application does not execute actions from responses that have not passed all required checks.
  5. 05Tests cover ambiguous inputs, missing fields, incorrect types, boundary values, and unexpected responses.
  6. 06The contract is versioned, and changes are reviewed with the components that consume it.
  7. 07Logs support failure investigation without retaining unnecessary personal or sensitive data.
  8. 08API and model compatibility have been checked against current official documentation and in the deployment environment.
10

Practical criteria for finalizing the design

Before considering the work complete, check that the structure addresses a real product need rather than simply a preference for receiving JSON. The contract should be small enough that every field has a clear use, but expressive enough to represent uncertainty, missing data, and outcomes that do not correspond to the task. If ambiguous cases have no safe representation, the design is not ready for production.

Make sure the consumer validates the response it receives, not just the instructions sent to the model. Check format, keys, types, and semantic rules separately. Define what is blocked, what is retried, and what is asked. Test those decisions with verifiable examples, including input that does not contain enough information and output that looks valid but contradicts the text.

Finally, document external uncertainties: specific API compatibility, schema limits, model coverage, and error formats. Resolve them by consulting official product documentation and testing the version that will be deployed. This lets the local contract remain a boundary for safety and maintenance even as the generation mode changes.

Open questions

  • The supplied sources do not fully detail the models, versions, and schema subsets supported by each mode; verify them in current official documentation for the selected implementation.
  • The supplied sources do not provide a unified specification for how providers represent refusals, generation errors, or incomplete responses; check handling for the specific API.
  • The example JSON schema and Python code are illustrative local proposals. They do not claim that the schema can be sent unchanged to a specific API.
  • Rules for resolving relative dates, confirming requests, or escalating ambiguous cases depend on the application and cannot be inferred from JSON syntax.
11

Keep exploring

11

Sources consulted

03

Corrections and transparency

If you spot incorrect or outdated information, send us a correction with the page and source we should review.

Submit a correction