🔗 References in OpenAPI
Clientele fully supports OpenAPI's $ref mechanism for reusing schema definitions across your API specification.
This page explains how references work and what Clientele does when generating clients with them.
What are $refs?
In OpenAPI, $ref (short for "reference") lets you define a schema, parameter, or response once and reuse it throughout your specification.
Here's a simple example:
{
"components": {
"schemas": {
"User": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" }
}
},
"UserList": {
"type": "object",
"properties": {
"users": {
"type": "array",
"items": { "$ref": "#/components/schemas/User" }
}
}
}
}
}
}
Instead of duplicating the User schema definition everywhere you need it, you use $ref to reference it.
How Clientele handles references
Clientele resolves all $ref declarations in your OpenAPI schema and generates properly-typed Python code using Pydantic models.
Types of references supported
Clientele handles $ref in all the places the OpenAPI specification allows:
1. Schema properties
When a property references another schema:
{
"Response": {
"properties": {
"user": { "$ref": "#/components/schemas/User" }
}
}
}
Generates:
class User(pydantic.BaseModel):
id: int
name: str
class Response(pydantic.BaseModel):
user: "User"
2. Array items
When array items reference a schema:
{
"UserList": {
"properties": {
"users": {
"type": "array",
"items": { "$ref": "#/components/schemas/User" }
}
}
}
}
Generates:
class UserList(pydantic.BaseModel):
users: list["User"]
3. Enum references
References to enum schemas work just as you'd expect:
{
"Status": {
"type": "string",
"enum": ["active", "inactive"]
},
"Response": {
"properties": {
"status": { "$ref": "#/components/schemas/Status" }
}
}
}
Generates:
class Status(str, enum.Enum):
ACTIVE = "active"
INACTIVE = "inactive"
class Response(pydantic.BaseModel):
status: "Status"
4. Response references
OpenAPI lets you define reusable responses in components/responses:
components:
responses:
ErrorResponse:
description: Standard error response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
Error:
type: object
properties:
message:
type: string
paths:
/users:
get:
responses:
'400':
$ref: '#/components/responses/ErrorResponse'
Clientele resolves the response reference and generates the corresponding schema just once:
class Error(pydantic.BaseModel):
message: str
5. Parameter references
You can define reusable parameters:
components:
parameters:
PageNumber:
name: page
in: query
schema:
type: integer
paths:
/users:
get:
parameters:
- $ref: '#/components/parameters/PageNumber'
Clientele includes these in the generated function signatures or header classes.
6. Composed schemas (allOf)
OpenAPI's allOf lets you compose schemas from multiple references:
components:
schemas:
BaseUser:
type: object
properties:
id:
type: integer
UserDetails:
type: object
properties:
email:
type: string
FullUser:
allOf:
- $ref: '#/components/schemas/BaseUser'
- $ref: '#/components/schemas/UserDetails'
Generates a single merged schema:
class FullUser(pydantic.BaseModel):
id: int # From BaseUser
email: str # From UserDetails
Nested references
Clientele handles deeply nested references without any issues:
{
"Comment": {
"properties": {
"author": { "$ref": "#/components/schemas/User" }
}
},
"Post": {
"properties": {
"comments": {
"type": "array",
"items": { "$ref": "#/components/schemas/Comment" }
}
}
}
}
Generates properly typed nested structures:
class User(pydantic.BaseModel):
id: int
name: str
class Comment(pydantic.BaseModel):
author: "User"
class Post(pydantic.BaseModel):
comments: list["Comment"]
Working with generated code
The generated code using references works seamlessly at runtime:
from my_api import schemas
# Create a user
user = schemas.User(id=1, name="Alice")
# Use in a response
response = schemas.Response(user=user)
# Type checking works!
reveal_type(response.user) # Revealed type is "User"
# IDE auto-completion works too
response.user.name # ← Your IDE suggests 'id' and 'name'
Why forward references?
You might notice that Clientele sometimes uses forward references (strings) for schema types:
user: "User" # ← String, not direct reference
This is intentional and solves a common problem in Python: circular dependencies.
When schemas reference each other, Python needs to know about both classes before they're used. Forward references let us define all the schemas in a single file without worrying about the order.
Pydantic automatically resolves these forward references at runtime by calling model_rebuild() at the end of the schemas file:
# At the end of schemas.py
subclasses: list[typing.Type[pydantic.BaseModel]] = get_subclasses_from_same_file()
for c in subclasses:
c.model_rebuild() # ← Resolves all forward references
This happens automatically - you don't need to do anything special to use the generated schemas.
Real-world example
Here's a complete example showing multiple reference types working together:
OpenAPI Schema
{
"components": {
"schemas": {
"UserRole": {
"type": "string",
"enum": ["admin", "user", "guest"]
},
"User": {
"type": "object",
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
"role": { "$ref": "#/components/schemas/UserRole" }
}
},
"TeamResponse": {
"type": "object",
"properties": {
"name": { "type": "string" },
"members": {
"type": "array",
"items": { "$ref": "#/components/schemas/User" }
},
"owner": { "$ref": "#/components/schemas/User" }
}
}
}
}
}
Generated Code
import enum
import pydantic
class UserRole(str, enum.Enum):
ADMIN = "admin"
USER = "user"
GUEST = "guest"
class User(pydantic.BaseModel):
id: int
name: str
role: "UserRole"
class TeamResponse(pydantic.BaseModel):
name: str
members: list["User"]
owner: "User"
# Forward references resolved automatically
subclasses = get_subclasses_from_same_file()
for c in subclasses:
c.model_rebuild()
Usage
from my_api import schemas
# Create users with enum roles
admin = schemas.User(
id=1,
name="Alice",
role=schemas.UserRole.ADMIN
)
member = schemas.User(
id=2,
name="Bob",
role=schemas.UserRole.USER
)
# Create a team response
team = schemas.TeamResponse(
name="Engineering",
members=[admin, member],
owner=admin
)
# Everything is properly typed
assert isinstance(team.owner, schemas.User)
assert team.owner.role == schemas.UserRole.ADMIN
Summary
Clientele handles all forms of $ref in OpenAPI schemas:
| Reference Type | Location | Supported | Example |
|---|---|---|---|
| Schema in property | properties.user.$ref |
✅ | user: "User" |
| Schema in array | items.$ref |
✅ | list["User"] |
| Enum reference | properties.status.$ref |
✅ | status: "Status" |
| Response reference | responses.400.$ref |
✅ | Schema generated |
| Parameter reference | parameters.$ref |
✅ | Included in functions |
| allOf composition | allOf[n].$ref |
✅ | Merged into one schema |
You don't need to do anything special to handle references in your OpenAPI schema - Clientele handles them automatically and generates clean, DRY code.