OpenAPI¶
SchemaRouter can import a common, production-oriented subset of OpenAPI 3.x from JSON or YAML.
Import¶
from schemarouter import SchemaRouter
router = await SchemaRouter.from_url(
"https://api.example.com/openapi.json",
kind="openapi",
)
The adapter imports operations, path/query/header parameters, JSON request-body properties, response
schemas, local component references, local reference chains, local Path Item references, and
read/write classification inferred from the HTTP method. Object properties and required fields
reachable through allOf are flattened for planner visibility while the original composition is
retained for runtime JSON Schema validation. For response schemas, object fields reachable through
oneOf / anyOf variants are also exposed as conditional planner-visible output fields. The
original composed response schema remains authoritative at runtime.
Same-origin and cross-origin servers¶
A same-origin runtime server can be bound automatically.
If the OpenAPI document declares a server on another origin, SchemaRouter imports the schema but does not silently grant execution authority.
router.bind_openapi(
"users_api",
base_url="https://api.example.com/",
trusted_headers={"Authorization": "Bearer ..."},
)
Schema credentials versus runtime credentials¶
router = await SchemaRouter.from_url(
"https://docs.example.com/openapi.json",
kind="openapi",
schema_headers={"X-Docs-Token": "..."},
base_url="https://api.example.com/",
trusted_headers={"Authorization": "Bearer ..."},
)
schema_headers never become runtime API headers, and trusted_headers are never exposed as
model-selectable arguments.
Typed JSON root request bodies¶
SchemaRouter keeps ordinary object request bodies ergonomic by flattening their declared properties
into named body parameters.
When an explicit JSON Schema cannot be flattened safely, the whole request payload is instead
represented as one typed body parameter. This covers:
- arrays;
- scalar JSON values;
- nullable/root
nullvalues; - general
oneOf/anyOfcompositions; - other explicit JSON Schema shapes that should remain intact.
For example, an array body:
becomes:
The full value is validated locally against the original schema and transmitted as the JSON root.
SchemaRouter does not wrap it as {"body": ...}. JSON null is also emitted as the literal
null body rather than being mistaken for an omitted request.
A required root body that is absent fails closed before a network request is made.
Discriminated JSON request bodies¶
SchemaRouter does not flatten arbitrary oneOf / anyOf request bodies because fields from
different variants could be combined into an invalid request. They are preserved as one typed root
body parameter instead.
For a strictly tagged oneOf, SchemaRouter additionally recognizes the discriminator contract
when:
- the schema declares
discriminator.propertyName; - every branch is object-like;
- every branch requires that discriminator property;
- every branch constrains it with a unique
constor single-valueenum.
Example:
schema:
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
discriminator:
propertyName: kind
The planner still sees one parameter carrying the original oneOf schema and discriminator
metadata. The body remains a single object through planning and validation. SchemaRouter validates the whole
object against the original composed JSON Schema, then the OpenAPI invoker sends that object as the
JSON request root. It never rewrites the payload as {"body": ...}.
This also works with a provider-neutral ModelQueryAnalyzer: an application-owned GPT, Gemini,
Claude, or other structured-output client may propose the complete body object, but unknown
arguments and schema-invalid variants are rejected locally before execution.
A discriminator mapping by itself is not sufficient. SchemaRouter requires the branch schemas to prove their own unique tags so a stale or incorrect mapping cannot weaken the local contract.
OpenAPI 3.0 nullable¶
OpenAPI 3.0 uses nullable: true instead of JSON Schema's null type. When type is declared
in the same Schema Object, SchemaRouter compiles the 3.0 semantics into an ordinary JSON Schema type
union:
becomes:
Other constraints remain authoritative. For example, an enum that omits null can still
reject null, matching OpenAPI 3.0's rule that other constraints retain their behavior.
Normalization is recursive through component schemas and the bounded external-reference bundle. Examples/default values and arbitrary extension payloads are not rewritten.
A nullable object request body remains a typed root body rather than being flattened, because
the JSON root itself may legally be null.
OpenAPI 3.1 documents are not rewritten: they should express nullability with JSON Schema types,
for example type: ["string", "null"].
Parameter serialization¶
SchemaRouter compiles the OpenAPI default parameter styles into the endpoint contract and preserves them in the schema fingerprint:
| Location | Supported style | Default explode | Supported values |
|---|---|---|---|
| path | simple |
false |
scalar, array, object |
| query | form |
true |
scalar, array, object |
| header | simple |
false |
scalar, array, object |
Examples:
path simple array:
["a", "b"] -> /a,b
query form array, explode=true:
["red", "blue"] -> ?tag=red&tag=blue
query form object, explode=false:
{"role":"admin","active":true}
-> ?filter=role,admin,active,true
header simple object, explode=true:
{"role":"admin","active":true}
-> X-Meta: role=admin,active=true
SchemaRouter deliberately fails closed for parameter serialization modes it does not yet emit
exactly, including non-default styles such as matrix, label, spaceDelimited,
pipeDelimited, and deepObject. Query parameters with allowReserved: true are also
rejected rather than silently changing reserved-character semantics.
The compatibility report exposes these cases before execution as parameter_style or
allow_reserved findings.
Parameter collisions¶
OpenAPI identifies parameters by both name and location. If an operation defines the same wire name in multiple places, SchemaRouter creates distinct logical argument keys.
The transport maps those keys back to the original wire name.
Local and same-document references¶
Nested #/components/... reference chains are resolved for planner-side schema discovery while
their component root remains available to runtime JSON Schema validation, including array item
schemas and recursive structures.
When a document is loaded from a URL, URI references that resolve back to that exact document are
normalized to local JSON Pointer references. For example,
./openapi.json#/components/schemas/User is treated as a local reference when the loaded resource
is that same openapi.json.
SchemaRouter does not fetch cross-document references by default. Trusted callers may opt in to bounded same-origin resolution:
router = await SchemaRouter.from_url(
"https://docs.example.com/openapi.json",
kind="openapi",
openapi_external_refs=True,
)
The resolver follows relative/absolute HTTP(S) references only when they stay on the entry
document's origin. It reuses schema_headers only after this explicit opt-in and applies independent
limits for recursion depth, unique referenced documents, aggregate referenced bytes, per-document
bytes, and redirects.
Defaults:
openapi_ref_max_depth = 3
openapi_ref_max_documents = 8
openapi_ref_max_bytes = 10 MiB
per referenced document = 5 MiB
redirects per document = 5
These bounds can be reduced by trusted application code through the corresponding
SchemaRouter.from_url() / add_url() keyword arguments.
Referenced JSON/YAML documents are fetched completely, indexed as JSON Schema resources, rewritten into a local in-memory bundle, and then consumed through the same local-ref parser and runtime JSON Schema validator. This avoids fragment-only parsing and keeps execution schemas self-contained.
With openapi_external_refs=True, the bounded resolver understands static JSON Schema resource
scope:
- same-origin absolute or relative
$idvalues rebase descendant$refresolution; - nested
$idvalues are indexed as virtual resources inside the already loaded document; - static
$anchorfragments such asschema.json#Userresolve to their indexed subschema; - fetched/rebased resources remain confined to the entry document's origin;
- resolved
$refvalues are rewritten to local JSON Pointers; $idand$anchorare removed from the final runtime bundle after rewriting so validation does not trigger a second external-resolution path.
The current bounded resolver intentionally fails closed for:
- cross-origin referenced documents or cross-origin
$idbase URIs; $idvalues with fragments;- missing, duplicate, or invalid static anchors;
$dynamicRef,$dynamicAnchor,$recursiveRef, and$recursiveAnchor;- depth/document/byte limit exhaustion;
- unstructured referenced content.
Dynamic JSON Schema scope is deliberately excluded because statically rewriting it as an ordinary anchor could change validation semantics.
Current common subset¶
Supported paths include:
- OpenAPI 3.x JSON and YAML;
- operations under
paths; - path/query/header parameters with spec-faithful default
simple/formserialization; - object-like JSON request bodies;
- JSON responses;
- local component/path-item reference chains;
- same-document URI-reference normalization;
- explicitly enabled bounded same-origin cross-document
$refbundling; - bounded same-origin JSON Schema
$idrebasing and static$anchorresolution; - OpenAPI 3.0
nullable: truenormalization whentypeis declared in the same Schema Object; - planner-side object-property/required flattening through
allOf; - planner-visible response-field discovery across
oneOf/anyOfobject variants while preserving composed runtime validation; - any explicit non-flattenable JSON request schema as one typed root-body parameter, including
arrays, scalars, nullable roots, and composed
oneOf/anyOfbodies; - discriminator recognition for strictly tagged
oneOfobject bodies; - explicit cross-origin binding;
- runtime origin confinement.
Dynamic JSON Schema references/anchors and automatic planner-side schema-variant selection remain follow-up work. Variant request bodies stay intentionally unflattened even though they are executable as one typed root body. Response variant fields may be selected for projection, but a field that is absent from the actual validated response variant is simply absent from the projected result. Unsupported constructs should not be guessed.
Runtime response bound¶
OpenAPI runtime responses are streamed and capped at 16 MiB by default before JSON/text decoding.
The bound applies even when a server omits or lies about Content-Length.
When constructing OpenAPIRemoteInvoker manually, trusted local code may choose a smaller or
larger positive integer through max_response_bytes. Keep the limit appropriate for the endpoint
contract; a high limit weakens protection against unexpectedly large remote responses.