Practical Guide to OpenAPI $ref Examples and Common Errors

OpenAPI $ref examples and common errors: Local, file, and URL references with tips to avoid unresolved refs, sibling property loss, and circular loops.

Jump to section

Jump to section

Jump to section

The $ref keyword in OpenAPI specifications can make or break your SDK generation and MCP server creation. At Stainless, we've seen countless specs where broken references, circular dependencies, and poor organization lead to unusable generated code, forcing developers to write custom workarounds instead of leveraging clean, type-safe SDKs.

This guide covers the mechanics of $ref usage, from basic local references to complex external file structures, plus the common pitfalls that break code generation. You'll learn how proper reference hygiene directly impacts SDK quality, MCP server reliability, and the overall developer experience when integrating with your API.
The $ref keyword lets you reuse schemas to keep your OpenAPI specification DRY, but misusing it can lead to broken SDKs and confusing MCP servers when converting complex OpenAPI specs to MCP servers. Getting references right is the key to generating clean, predictable, and maintainable code from your API spec using the Stainless SDK generator.

What $ref does in OpenAPI

In OpenAPI, $ref is a keyword used to reference and reuse schema definitions from elsewhere in the same document or from external files. This practice keeps your API specification DRY (Don't Repeat Yourself) and ensures that a single source of truth for a data model, like a User object, is used consistently across all endpoints. This consistency is critical for generating clean, predictable SDKs and MCP servers where a User is always the same type.

Understand basic $ref syntax

A $ref value is a JSON Reference, which contains a URI pointing to the referenced value. If the reference is within the same document, the URI is followed by a # and a JSON Pointer, which is a string that navigates through the keys of the JSON or YAML document.

For example, #/components/schemas/User points to the User object inside the schemas object, which is inside the components object at the root of the document.

# OpenAPI Spec
paths:
  /users/{id}:
    get:
      summary: Get a user by ID
      responses:
        '200':
          description: A single user.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User' # This is the reference

components:
  schemas:
    User: # This is the target schema
      type: object
      properties:
        id:
          type: string
        name:
          type

Compare reference and pointer semantics

The part of the string after the # is the JSON Pointer. It acts like a file path for your spec's structure.

  • Path segments: Each segment starts with a / and corresponds to a key in your object.

  • Case sensitivity: JSON Pointers are case-sensitive, so /schemas/user is different from /schemas/User.

  • Array indexing: To reference an element in an array, you use its zero-based index, like /parameters/0.

Write correct $ref paths

Correctly structuring your $ref paths is fundamental. Here are the most common patterns, from local references within a single file to remote references across the web.

Use local references

This is the most common and straightforward way to use $ref. You define reusable components like schemas, parameters, or responses in the components section of your spec and reference them from anywhere else in the same file.

# openapi.yaml
paths:
  /users:
    post:
      summary: Create a new user
      requestBody:
        $ref: '#/components/requestBodies/NewUser' # Reference a request body
      responses:
        '201':
          $ref: '#/components/responses/UserResponse' # Reference a response

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
          format: email

  requestBodies:
    NewUser:
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/User' # Nested reference to a schema

  responses:
    UserResponse:
      description: The created user object.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/User'

Use external file references

For larger APIs, keeping everything in one file becomes unmanageable when you create OpenAPI specs. You can split your spec into multiple files and use relative paths in your $ref values. This improves organization and makes version control diffs much cleaner.

Imagine this file structure:


You can reference the user schema from your main file.

# openapi.yaml
paths:
  /users/{id}:
    get:
      summary: Get a user by ID
      responses:
        '200':
          description: A single user.
          content:
            application/json:
              schema:
                $ref: './schemas/user.yaml' # Reference the external file
# schemas/user.yaml
type: object
properties:
  id:
    type: string
  name:
    type: string
  email:
    type: string
    format

Use remote URL references

You can also reference a spec hosted on a different server, like a common objects library for your entire organization. The value of $ref is simply the full URL to the spec.

paths:
  /users/{id}:
    get:
      summary: Get a user by ID
      responses:
        '200':
          description: A single user.
          content:
            application/json:
              schema:
                $ref: 'https://api.example.com/schemas/user.json'

Heads up: When using remote references, be mindful of availability and potential CORS issues. Modern tooling often "bundles" these remote references into a single file at build time to avoid runtime dependencies.

Escape special characters

If a key in your spec contains a tilde (~) or a forward slash (/), you must escape them in the JSON Pointer. This is rare but important for correctness.

  • ~ becomes ~0

  • / becomes ~1

For a schema defined under the key a/b, the reference would be #/components/schemas/a~1b.

Fix common $ref errors

Using $ref can introduce a few common errors. Here’s how to spot and fix them, with examples of what broken and working code looks like.

Resolve unresolved references

This is the most frequent error, often caused by a simple typo in the path. Validation tools will report something like "Could not resolve reference" or "unresolved reference".

Broken: The path points to .../schema/ instead of .../schemas/.

# Before
schema:
  $ref: '#/components/schema/User' # Typo: "schema" should be "schemas"

Working: Correct the path to match the structure of your document.

# After
schema:
  $ref: '#/components/schemas/User' # Corrected path

Avoid sibling property loss

In OpenAPI versions before 3.1, any properties defined at the same level as a $ref (siblings) are ignored by tools. This is a common source of confusion when trying to add a description or example.

Broken: The description here will be ignored by most tools.

# Before
properties:
  user:
    $ref: '#/components/schemas/User'
    description: The user associated with this record. # This will be ignored

Working: Use the allOf keyword to combine the reference with additional properties. This correctly extends the referenced schema.

# After
properties:
  user:
    allOf:
      - $ref: '#/components/schemas/User'
    description: The user associated with this record. # This now works

Avoid invalid locations

You cannot use $ref to replace certain top-level sections of your OpenAPI document, such as the paths object or the info object. References are meant for reusable components, not for structuring the entire spec.

Broken: You cannot reference the entire paths object.

# Before
paths:
  $ref: './paths/all_paths.yaml' # Invalid usage

Working: Structure your files so that you are referencing valid component types, like individual path items, schemas, or parameters.

Break circular references

A circular reference occurs when a schema refers to itself, creating an infinite loop. For example, a User schema has a manager property which is also a User, which in turn has its own manager.

Broken: This can cause code generators to enter an infinite loop.

# Before
User:
  type: object
  properties:
    name:
      type: string
    manager:
      $ref: '#/components/schemas/User' # Circular reference

Working: Break the loop by making the recursive property nullable or by using allOf to signal a more complex relationship that tools can handle more gracefully. For SDK generation, this often results in a type that can be lazily evaluated.

# After
User:
  type: object
  properties:
    name:
      type: string
    manager:
      nullable: true # Making it nullable breaks the strict loop
      allOf:
        - $ref: '#/components/schemas/User'

Improve SDKs and MCP with $ref

How you use $ref directly impacts the quality and usability of the code you generate. Good reference hygiene leads to cleaner SDKs and more reliable MCP servers.

Organize components for reuse

When you define a schema like User in components and reference it everywhere a user object appears, code generators create a single, shared User type. Without this, you'd get dozens of slightly different, endpoint-specific types like GetUserResponse, CreateUserRequest, and UpdateUserPayload, cluttering your SDK.

Reduce SDK complexity with base schemas

You can use allOf to create base schemas for common patterns like pagination. This keeps your endpoint definitions clean and results in a more intuitive, inheritable structure in your SDK.

# A reusable pagination schema
PaginatedResponse:
  type: object
  properties:
    has_more:
      type: boolean
    next_cursor:
      type: string
      nullable: true

# An endpoint response using the base schema
UserListResponse:
  allOf:
    - $ref: '#/components/schemas/PaginatedResponse'
    - type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/User'

Version schemas without breaking clients

If you need to introduce a breaking change to a schema, you can create a new version and reference it, leaving the old one intact for backward compatibility.

  • #/components/schemas/v1/User

  • #/components/schemas/v2/User

This allows you to support multiple API versions from a single spec, and a well-designed SDK can expose both versions clearly to developers when you integrate SDK snippets with your API docs.

Prepare schemas for MCP tools

Many MCP clients, like those for Claude or Cursor, have limitations and may not correctly handle $ref or complex union types when you generate MCP servers from OpenAPI specs. For an MCP tool to work reliably, its input schema often needs to be fully self-contained.

  • Inlining: Good tooling can automatically "inline" all $refs during MCP server generation, creating a single, flat schema that all clients can understand.

  • Transformation: For more complex structures like anyOf, a generator can transform them into multiple, simpler tools, ensuring compatibility without sacrificing the clarity of your source spec.

Incorporating $ref in Stainless config (optional)

If you want to map your OpenAPI references directly into a Stainless configuration file, you can use $ref values in the models: section of your stainless.config.yaml. This bridges your spec to your Stainless resources and methods:

# stainless.config.yaml
resources:
  users:
    models:
      user: '#/components/schemas/User'  # Reference the OpenAPI schema
    methods:
      retrieve: get /users/{id}           # Map the GET operation

In this example, Stainless will generate a users resource with a user model based on the same User schema defined in your OpenAPI spec.

Validate $ref usage

Catching $ref errors early saves a lot of time. Integrating automated validation into your workflow is a must.

Run spectral rules

Spectral is a popular linter for OpenAPI. You can configure it to enforce strict reference rules, catching broken links before they cause problems.

Here is a basic .spectral.yaml to get started:

extends: spectral:oas
rules:
  oas3-valid-schema-example: off # Often noisy, can be enabled later
  unresolved-ref: error          # Fail on any broken reference

Bundle specs for distribution

Before distributing your spec or feeding it to a code generator, it's a good practice to bundle all external and remote references into a single file, especially when you edit configs and OpenAPI specs with branches. You can use a command line tool for this:

# Using Redocly CLI to bundle the spec
npx @redocly/cli bundle openapi.yaml -o

Read Stainless diagnostics

When you generate code, the diagnostics panel provides immediate feedback on your spec. It will flag unresolved references, unused schemas, and other potential issues with your $ref usage, often suggesting a one-click fix right in the studio. This tight feedback loop helps you correct errors as you work, not after a failed build.

Frequently asked questions about OpenAPI $ref

How do I debug “could not resolve reference” errors?

First, double-check the path for typos, paying close attention to case sensitivity and pluralization (e.g., schemas vs. schema). If it's an external file, verify the relative path is correct from the location of the root file.

Can I reference files behind authentication?

It's best to avoid this for runtime resolution. Instead, your CI/CD pipeline should fetch and bundle these protected files into your spec before it's used for documentation or code generation.

What changes in $ref between openapi 2.0 and 3.x?

The biggest change is the location of definitions, moving from a top-level definitions object in 2.0 to a more structured #/components/schemas/ in 3.0. OpenAPI 3.1 also officially supports sibling properties next to a $ref, removing the need for the allOf workaround in many cases.

Do circular references break Stainless SDKs?

Our generator is designed to handle circular references gracefully by creating types that can be lazily evaluated or are nullable, preventing infinite recursion. However, it's always a good practice to test the generated types to ensure they behave as you expect.

When should I inline a schema instead of referencing?

You should only inline a schema if it's truly a one-off definition that will never be reused, such as a very simple, endpoint-specific error response. For any object that represents a core concept in your API, always use a $ref.

Try Stainless Studio for instant diagnostics on your own OpenAPI spec. Get started for free.