By April 2026, OpenAI, Anthropic, and Gemini offered structured-output mechanisms, but their request fields, supported JSON Schema subsets, strictness, streaming events, and errors were not identical. The dangerous abstraction is a “universal parameter dictionary” that sends OpenAI-shaped fields to every provider. A durable design has one domain schema and three independent adapters, each honoring its official contract.

Define the domain before provider schemas

Suppose a product classifies support tickets. The domain declares a versioned result without importing any model SDK:

from typing import Literal
from pydantic import BaseModel, Field

class TicketDecisionV1(BaseModel):
    schema_version: Literal["ticket-decision.v1"]
    category: Literal["billing", "bug", "account", "other"]
    urgency: int = Field(ge=1, le=5)
    evidence: list[str] = Field(min_length=1, max_length=4)
    needs_human: bool

Enums come from the product, not from one model’s recommendation. Schema evolution needs a policy. An optional additive field may preserve compatibility, while a changed enum meaning or new required field creates v2. Store schema, provider, model id, adapter version, and prompt version with every decision.

Use descriptions to clarify semantics, but do not place secrets or dynamic user data in a reusable schema description. The prompt supplies request-specific context.

Let each adapter translate independently

The OpenAI adapter submits the schema through OpenAI’s structured text format. The Anthropic adapter uses the Claude structured-outputs contract. The Gemini adapter uses its response-schema configuration. All return TicketDecisionV1 or a shared application error, but they do not pretend request parameters are the same.

At startup or deployment, validate whether the domain schema fits a provider’s supported subset. If a keyword is unsupported, do not silently drop the constraint. Add explicit application post-validation and record the difference, simplify the schema deliberately, or reject that provider. Maintain a capability table for additional properties, enum, nesting, optional fields, array bounds, recursion, streaming, and refusal representation.

Provider documentation can change in place. Pin SDK versions and retain contract fixtures so a dependency upgrade cannot silently alter schema translation.

Validate twice

A provider guarantee or best effort that JSON matches a schema addresses syntax and some structure. Run domain checks after parsing. Evidence must come from input, urgency must satisfy deterministic policy, sensitive tickets must require a person, and text must fit storage and privacy limits. A model cannot authorize a refund or account change by selecting a schema-valid value.

Separate transport, provider refusal, unsupported schema, parse failure, domain validation, and policy denial. Only transient transport failures or a clearly repairable format problem suit bounded retry. A domain-policy failure usually routes to a person or fallback. Asking indefinitely until a model produces an accepted value converts validation into a suggestion.

Avoid coercion that hides errors. Converting the string "five" to integer 5 may be convenient, but strict machine interfaces should reject it unless the domain explicitly permits that representation.

Keep partial JSON out of business logic

JSON may be invalid until the final byte. A UI may present progress, but actions wait for complete parse and validation. Providers expose different stream events, so each adapter accumulates its native stream and commits a domain object at completion. Upstream code never depends on provider token or event names. On interruption, discard or mark the object incomplete.

If the product needs incremental structure, define an event protocol instead of parsing half JSON: classification_started, evidence_found, then decision_committed. Only the final event carries the validated object.

Evaluate portability

Run one data set, domain schema, prompt intent, and post-validation policy through all adapters. Compare parse success, domain acceptance, factual correctness, refusal, latency, tokens, and error classes. Prompts can be idiomatic per provider, but record differences. Do not force identical strings for superficial fairness, and do not secretly give one provider more evidence.

Include additional fields, invalid enum values, malicious requests to change the schema, long arrays, Unicode, empty output, refusal, and interrupted streams. Contract tests use mocked provider responses to validate adapters without paid calls. Scheduled integration evaluations cover real model behavior.

Treat fallback as a distributed operation

Before switching, verify data policy, model capability, and schema support. A timeout followed by fallback can produce two successful provider results, so use a request identity and idempotent commit. Record the chosen provider with the result and segment monitoring. Different models should not disappear into one aggregate metric.

The stable cross-model layer is not a common parameter bag. It is a versioned domain schema, separate adapters, a shared error taxonomy, and a second business-validation boundary. Respect each provider’s subset and streaming semantics, and explicitly revalidate or reject constraints it cannot express. Provider change then remains at the edge while business code consumes only objects it owns and understands.