Skip to main content

Crate openapiv3_resolve

Crate openapiv3_resolve 

Source
Expand description

§openapiv3-resolve

crates.io docs.rs

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 on OpenAPI. openapi.resolve_ref::<Schema>(ptr) takes a full pointer like #/components/schemas/Pet. The type argument decides which section is searched.
  • ResolveWithOpenAPI<T> — implemented on ReferenceOr<T> and ReferenceOr<Box<T>>; returns the inline item, or resolves the reference.
  • ResolveOptionalWithOpenAPI<T> — implemented on Option<R> for any resolvable R; resolve_optional returns Ok(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§

pub use indexmap;
pub use openapiv3;

Structs§

NestedSchema
A schema nested inside another schema.
ResolvedAnySchema
AnySchema with every $ref replaced by the schema it named.
ResolvedArrayType
ArrayType with a $ref in items replaced by the schema it named.
ResolvedComponents
Components with every entry resolved, including entries that were themselves a $ref to another entry.
ResolvedDiscriminator
Discriminator with every mapping value replaced by the schema it named.
ResolvedEncoding
Encoding with every $ref replaced by the item it named.
ResolvedHeader
Header with every $ref replaced by the item it named.
ResolvedMediaType
MediaType with every $ref replaced by the item it named.
ResolvedObjectType
ObjectType with every $ref replaced by the schema it named.
ResolvedOpenAPI
OpenAPI with every $ref in the document replaced by the item it named.
ResolvedOperation
Operation with every $ref replaced by the item it named.
ResolvedParameterData
ParameterData with every $ref replaced by the item it named.
ResolvedPathItem
PathItem with every $ref replaced by the item it named.
ResolvedPaths
Paths with every $ref replaced by the path item it named.
ResolvedRequestBody
RequestBody with every $ref replaced by the item it named.
ResolvedResponse
Response with every $ref replaced by the item it named.
ResolvedResponses
Responses with every $ref replaced by the response it named.
ResolvedSchema
Schema with every $ref replaced by the schema it named.
ResolvedSchemaData
SchemaData with the discriminator’s mapping targets resolved.
SchemaGuard
A borrow of a NestedSchema’s target; dereferences to the schema.
Shared
A resolved item, shared by every $ref that named it.

Enums§

ResolveError
Everything that can go wrong while resolving a $ref.
ResolvedAdditionalProperties
AdditionalProperties with a $ref replaced by the schema it named.
ResolvedParameter
Parameter with every $ref replaced by the item it named.
ResolvedParameterSchemaOrContent
ParameterSchemaOrContent with every $ref replaced by the item it named.
ResolvedSchemaKind
SchemaKind with every $ref replaced by the schema it named.
ResolvedType
Type with every $ref replaced by the schema it named.
Section
A section of an OpenAPI document that $ref pointers can name.

Constants§

MAX_REFERENCE_HOPS
How many $ref hops a single resolution may follow.

Traits§

Component
A type that $ref pointers can name.
Resolve
Resolves a $ref pointer against a whole document.
ResolveOptionalWithOpenAPI
Resolves an optional ReferenceOr<T> field.
ResolveWithOpenAPI
Resolves a ReferenceOr<T> (or its boxed variant) to the item it denotes.

Type Aliases§

ResolvedCallback
Callback with every $ref replaced by the item it named.