Expand description
§openapiv3-resolve
Reference resolution helpers for the openapiv3 crate.
This crate adds traits that resolve $ref pointers (e.g.
#/components/schemas/Pet) against an OpenAPI document, returning a
borrowed reference to the resolved item. It walks chains of references
transparently, supports the boxed variant ReferenceOr<Box<T>> used by fields
such as ArrayType::items, and reports why a reference could not be resolved
instead of collapsing every failure into “not found”.
§Usage
use openapiv3::{OpenAPI, StatusCode};
use openapiv3_resolve::{ResolveOptionalWithOpenAPI, ResolveWithOpenAPI};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let spec = r##"{
"openapi": "3.0.0",
"info": { "title": "Pets", "version": "1.0.0" },
"paths": {
"/pets": {
"get": {
"responses": { "200": { "$ref": "#/components/responses/PetList" } }
}
}
},
"components": {
"responses": {
"PetList": {
"description": "a list of pets",
"content": {
"application/json": { "schema": { "$ref": "#/components/schemas/Pet" } }
}
}
},
"schemas": {
"Pet": { "title": "Pet", "type": "string" }
}
}
}"##;
let openapi: OpenAPI = serde_json::from_str(spec)?;
let path = openapi.paths.paths.get("/pets").ok_or("no /pets")?;
let get = path.resolve(&openapi)?.get.as_ref().ok_or("no GET")?;
// `responses` holds a `ReferenceOr<Response>`; `resolve` follows the `$ref`.
let response = get
.responses
.responses
.get(&StatusCode::Code(200))
.ok_or("no 200")?
.resolve(&openapi)?;
// `schema` is an `Option<ReferenceOr<Schema>>`: absent is not an error,
// so it gets its own method.
let media = response.content.get("application/json").ok_or("no JSON body")?;
let schema = media.schema.resolve_optional(&openapi)?.ok_or("untyped body")?;
assert_eq!(schema.schema_data.title.as_deref(), Some("Pet"));
Ok(())
}§Traits
Resolve— implemented onOpenAPI.openapi.resolve_ref::<Schema>(ptr)takes a full pointer like#/components/schemas/Pet. The type argument decides which section is searched.ResolveWithOpenAPI<T>— implemented onReferenceOr<T>andReferenceOr<Box<T>>; returns the inline item, or resolves the reference.ResolveOptionalWithOpenAPI<T>— implemented onOption<R>for any resolvableR;resolve_optionalreturnsOk(None)for an absent field and an error only for a reference that is present but broken.
Resolvable targets are the nine #/components sections plus #/paths, listed
by the Section enum. A $ref is read as a URI reference: the fragment is
percent-decoded first, then RFC 6901 unescaped, so the path /pets/{id} is
reachable as #/paths/~1pets~1%7Bid%7D.
The Component trait that maps a Rust type to its section is sealed, so the
two can never disagree. Note that openapiv3::Callback is a transparent alias
for IndexMap<String, PathItem> rather than a distinct type, so any value of
that shape resolves as a callback.
§Resolving a whole document
ResolvedOpenAPI is the document with every $ref followed up front: a
mirror of openapiv3::OpenAPI in which each ReferenceOr<T> has become a
Shared<ResolvedT> (or Shared<T> for Example, Link and
SecurityScheme, which hold no references). Shared dereferences to the
item. Every reference to the same component shares one allocation, so
Shared::as_ptr tells whether two sites named the same component, and
components holds those same allocations. A schema’s discriminator.mapping
is resolved too: a value containing # is followed as a $ref, any other
value is the name of a schema under components/schemas, and either way the
entry becomes a NestedSchema edge to that schema. On a oneOf or anyOf
schema each value must name one of the alternatives and the entry is that
alternative’s edge; a value naming anything else, or a dangling one, fails the
document.
use openapiv3::OpenAPI;
use openapiv3_resolve::{ResolvedOpenAPI, ResolvedParameterSchemaOrContent, Shared};
use std::ptr;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let spec = r##"{
"openapi": "3.0.0",
"info": { "title": "Pets", "version": "1.0.0" },
"paths": {
"/pets": {
"get": {
"parameters": [ { "$ref": "#/components/parameters/Limit" } ],
"responses": {}
}
}
},
"components": {
"parameters": {
"Limit": {
"name": "limit", "in": "query",
"schema": { "$ref": "#/components/schemas/Limit" }
}
},
"schemas": { "Limit": { "type": "integer" } }
}
}"##;
let openapi: OpenAPI = serde_json::from_str(spec)?;
let resolved = ResolvedOpenAPI::try_from(&openapi)?;
let get = resolved.paths().paths["/pets"].get.as_ref().ok_or("no GET")?;
let limit = get.parameters.first().ok_or("no parameter")?;
let ResolvedParameterSchemaOrContent::Schema(schema) = &limit.parameter_data().format else {
return Err("limit has content, not a schema".into());
};
let components = resolved.components().ok_or("no components")?;
assert!(ptr::eq(Shared::as_ptr(limit), Shared::as_ptr(&components.parameters["Limit"])));
assert!(ptr::eq(Shared::as_ptr(schema), Shared::as_ptr(&components.schemas["Limit"])));
Ok(())
}Resolution fails on the first reference that does not resolve, with the same
ResolveError the borrowing traits return.
§Recursive schemas
A schema nested inside another schema is a NestedSchema; get() borrows
it. Nearly always that is a schema like any other. The exception is a $ref
that points back at a schema which contains it (a tree node whose children
are nodes, say): that edge is_recursive(), and holds only a weak pointer,
because a cycle of owning pointers would never be freed. get() still just
works, because the target lives in components and the edge can only be
reached by borrowing from the document.
That guarantee is why the document is only ever borrowed from: its fields are
behind getters, and Shared is not Clone, so no piece of it can outlive
the whole. Put the document in an Arc to share it.
use openapiv3::OpenAPI;
use openapiv3_resolve::{ResolvedOpenAPI, ResolvedSchemaKind, ResolvedType};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let spec = r##"{
"openapi": "3.0.0",
"info": { "title": "Trees", "version": "1.0.0" },
"paths": {},
"components": {
"schemas": {
"Node": {
"title": "Node",
"type": "object",
"properties": { "next": { "$ref": "#/components/schemas/Node" } }
}
}
}
}"##;
let openapi: OpenAPI = serde_json::from_str(spec)?;
let resolved = ResolvedOpenAPI::try_from(&openapi)?;
let node = &resolved.components().ok_or("no components")?.schemas["Node"];
let ResolvedSchemaKind::Type(ResolvedType::Object(object)) = &node.schema_kind else {
return Err("Node is not an object".into());
};
let next = &object.properties["next"];
assert!(next.is_recursive());
// `get` borrows the target; no `Option`, no match.
assert_eq!(next.get().schema_data.title.as_deref(), Some("Node"));
Ok(())
}Which edge of a cycle is the recursive one is decided by document order: the
first $ref, walking components then paths, that closes the cycle. A
component that contains itself in any other way (a header whose content
encoding names that same header is the only one a document can express) has
no finite tree form and fails with CyclicReference.
§Errors
Every failure is a distinct ResolveError variant, so a caller can tell a
typo in the document (NotFound, SectionMismatch) from a reference this
crate structurally does not follow (ExternalDocument, PointerTooDeep) from
a document that is broken (ReferenceChainTooLong, which is what a cycle of
bare $refs looks like, and CyclicReference for a non-schema component
that contains itself, which only a full resolution can detect).
Reference chains are walked iteratively and capped at MAX_REFERENCE_HOPS, so
a cyclic document returns an error rather than overflowing the stack.
§Thread safety
OpenAPI and every resolvable component are Send + Sync, resolution takes
&self, and the returned borrow is Send + Sync too — so a resolved reference
can be held across an .await in a Send future.
Resolving a #/components/... pointer allocates nothing, however long the
reference chain. Pointers carrying an escape (~0, ~1, %XX) are the
exception: the decoded name has to be built. Both are pinned by a test.
§Minimum supported Rust version
1.85, which is the floor indexmap imposes rather than anything this crate
needs, and it is checked by its own CI job. A dependency raising its MSRV
raises this one; that is a minor version bump.
§License
Licensed under the MIT license.
Re-exports§
Structs§
- Nested
Schema - A schema nested inside another schema.
- Resolved
AnySchema AnySchemawith every$refreplaced by the schema it named.- Resolved
Array Type ArrayTypewith a$refinitemsreplaced by the schema it named.- Resolved
Components Componentswith every entry resolved, including entries that were themselves a$refto another entry.- Resolved
Discriminator Discriminatorwith every mapping value replaced by the schema it named.- Resolved
Encoding Encodingwith every$refreplaced by the item it named.- Resolved
Header Headerwith every$refreplaced by the item it named.- Resolved
Media Type MediaTypewith every$refreplaced by the item it named.- Resolved
Object Type ObjectTypewith every$refreplaced by the schema it named.- Resolved
OpenAPI OpenAPIwith every$refin the document replaced by the item it named.- Resolved
Operation Operationwith every$refreplaced by the item it named.- Resolved
Parameter Data ParameterDatawith every$refreplaced by the item it named.- Resolved
Path Item PathItemwith every$refreplaced by the item it named.- Resolved
Paths Pathswith every$refreplaced by the path item it named.- Resolved
Request Body RequestBodywith every$refreplaced by the item it named.- Resolved
Response Responsewith every$refreplaced by the item it named.- Resolved
Responses Responseswith every$refreplaced by the response it named.- Resolved
Schema Schemawith every$refreplaced by the schema it named.- Resolved
Schema Data SchemaDatawith the discriminator’s mapping targets resolved.- Schema
Guard - A borrow of a
NestedSchema’s target; dereferences to the schema. - Shared
- A resolved item, shared by every
$refthat named it.
Enums§
- Resolve
Error - Everything that can go wrong while resolving a
$ref. - Resolved
Additional Properties AdditionalPropertieswith a$refreplaced by the schema it named.- Resolved
Parameter Parameterwith every$refreplaced by the item it named.- Resolved
Parameter Schema OrContent ParameterSchemaOrContentwith every$refreplaced by the item it named.- Resolved
Schema Kind SchemaKindwith every$refreplaced by the schema it named.- Resolved
Type Typewith every$refreplaced by the schema it named.- Section
- A section of an OpenAPI document that
$refpointers can name.
Constants§
- MAX_
REFERENCE_ HOPS - How many
$refhops a single resolution may follow.
Traits§
- Component
- A type that
$refpointers can name. - Resolve
- Resolves a
$refpointer against a whole document. - Resolve
Optional With OpenAPI - Resolves an optional
ReferenceOr<T>field. - Resolve
With OpenAPI - Resolves a
ReferenceOr<T>(or its boxed variant) to the item it denotes.
Type Aliases§
- Resolved
Callback Callbackwith every$refreplaced by the item it named.