You can reference schemas and components defined in other OpenAPI documents, even if they’re in separate files.
Here’s a demo of how that works. Imagine you have two OpenAPI files:
common-schemas.yaml:
openapi: 3.0.0
info:
title: Common Schemas
version: 1.0.0
components:
schemas:
Address:
type: object
properties:
street:
type: string
city:
type: string
zipCode:
type: string
required:
- street
- city
- zipCode
And user-service.yaml:
openapi: 3.0.0
info:
title: User Service API
version: 1.0.0
paths:
/users/{userId}:
get:
summary: Get a user by ID
parameters:
- name: userId
in: path
required: true
schema:
type: integer
format: int64
responses:
'200':
description: User details
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
properties:
id:
type: integer
format: int64
name:
type: string
shippingAddress:
$ref: '../common-schemas.yaml#/components/schemas/Address'
billingAddress:
$ref: '../common-schemas.yaml#/components/schemas/Address'
required:
- id
- name
- shippingAddress
- billingAddress
Notice how user-service.yaml references Address from common-schemas.yaml using a relative file path (../common-schemas.yaml). This allows you to keep your OpenAPI definitions modular and organized, especially in larger projects. The $ref path is resolved relative to the location of the file containing the $ref.
This pattern is incredibly useful for enforcing consistency across different services or modules. Instead of redefining common data structures like Address, ErrorResponse, or UserIdentity in every API spec, you define them once in a shared file and then reference them everywhere. This drastically reduces duplication, makes updates much easier (change it in one place, it’s updated everywhere), and improves the overall maintainability of your API documentation.
The core mechanism is the $ref keyword, which acts as a pointer. When a tool processes these OpenAPI documents (like a code generator, validator, or UI renderer), it follows these $ref pointers. If the pointer points to a local fragment within the same document (starting with #), it resolves it directly. If it points to an external resource (a URL or a file path), the processor fetches that resource and then resolves the fragment within it. The ability to use relative file paths is what enables cross-file referencing.
The paths used in $ref follow a familiar URI-like structure. For local references within the same document, you use a JSON Pointer syntax, like #/components/schemas/MySchema. For external references, you combine a URI (which can be a file path like ../common-schemas.yaml) with a JSON Pointer fragment, separated by a #. So, ../common-schemas.yaml#/components/schemas/Address means "go up one directory, find common-schemas.yaml, and within that file, look for the Address schema under components/schemas."
When you have multiple files, the processing tool needs to be aware of the base URI of the document it’s currently parsing to correctly resolve relative paths. Most OpenAPI tooling handles this automatically by treating the directory of the current file as the base. This means if user-service.yaml is in /api/v1/ and common-schemas.yaml is in /api/, the path ../common-schemas.yaml correctly points to the common schemas.
The most surprising thing about this feature is how robustly it handles various referencing scenarios, including circular references (though these can sometimes lead to infinite loops in processing if not handled carefully by the tool) and referencing across different protocols or remote locations. However, for practical, maintainable API design, sticking to relative file paths within a project is the most common and recommended approach. It keeps your documentation self-contained and easy to manage.
The next challenge you’ll face is how to manage the resolution of these references when they become deeply nested or involve many files across different directories.