| name | openapi-schema-patterns |
| description | Advanced OpenAPI schema patterns including allOf, naming hierarchies, and context separation |
API Specification - Schema Patterns
Rules for schema design patterns and naming conventions
Pattern 1: allOf with Single $ref for Description Override
The Problem
OpenAPI ignores sibling properties when using $ref:
items:
$ref: '#/components/schemas/UserGroup'
description: Groups the user belongs to
The Solution
Use allOf wrapper to preserve description:
items:
allOf:
- $ref: '#/components/schemas/UserGroup'
description: Groups the user belongs to
When to Use
Use allOf with single $ref when you need to:
- Add or override
description on a referenced schema
- Add additional metadata (example, deprecated, etc.)
- Change any property that would be lost as sibling to
$ref
When NOT to Use
Do NOT use allOf for actual schema composition - that's a different pattern:
UserInfo:
allOf:
- $ref: '#/components/schemas/User'
- type: object
properties:
additionalField: string
Pattern 2: Schema Naming Hierarchy
Naming Convention
Schemas follow this hierarchy based on usage context:
| Pattern | When to Use | Contains | Example |
|---|
<Resource> | List operation results | Core fields for list view | User, Group, Acu |
<Resource>Info | Get operation results (if different) | Full details, more than list | UserInfo, AcuInfo |
<Resource>Summary | Nested references (detailed) | More than Ref, less than full | UserSummary |
<Resource>Ref | Nested references (minimal) | Just id + name | OrganizationRef, ZoneSiteRef |
<Context><Resource> | Resource within specific context | The Resource (2nd word) as viewed from Context (1st word) | GroupUser (User from Group), ZoneEntry (Entry from Zone) |
Examples
User:
properties:
id: string
status: string
email: string
UserInfo:
allOf:
- $ref: '#/components/schemas/User'
- type: object
properties:
organizationId: string
lastLoginAt: string
customFields: object
OrganizationRef:
properties:
id: string
name: string
GroupUser:
properties:
id: string
name: string
email: string
role: string
Naming Rules
- Base Resource (
User, Group): Singular, PascalCase, used in list operations
- Info Variant (
UserInfo): Add Info suffix when get returns more fields than list
- Ref Variant (
OrganizationRef): Add Ref suffix for minimal nested references
- Summary Variant (
UserSummary): Add Summary for medium-detail nested references
- Context Prefix (
GroupUser): <Context><Resource> where Resource (2nd word) is what's returned, Context (1st word) is the perspective. So GroupUser returns a User as seen from Group context (NOT UserGroup which would return a Group!)
Pattern 3: Follow Original API Naming (After camelCase)
CRITICAL: We wrap external APIs - we don't design from scratch. Follow vendor API naming after normalizing to camelCase.
Field Names
userProfilePicture:
type: string
avatarUrl:
type: string
Rule: Convert snake_case/kebab-case to camelCase, but keep the semantic name from vendor API.
Boolean Fields
isActive: boolean
hasAccess: boolean
canOverride: boolean
active: boolean
Rule: Keep vendor's boolean naming convention (is/has/can/supports) exactly as they define it.
Timestamp Fields
createdAt: string
lastLoginTime: string
statusUpdated: string
createdAt: string
lastLoginAt: string
statusUpdatedAt: string
Rule: Keep vendor's timestamp naming pattern. If they use *_at, use *At. If they use *_time, use *Time.
Count Fields
userCount: integer
totalEntries: integer
numZones: integer
userCount: integer
entryCount: integer
zoneCount: integer
Rule: Keep vendor's count field naming (count/total/num) as they define it.
Path Parameters
paths:
/users/{userId}:
parameters:
- name: userId
paths:
/templates/{templateName}:
parameters:
- name: templateName
paths:
/roles/{roleArn}:
parameters:
- name: roleArn
Rule: Use whatever identifier type the vendor API uses (id/name/arn/key). Convert to camelCase but keep semantic meaning.
Security Scopes
securitySchemes:
oauth:
flows:
authorizationCode:
scopes:
user:read: Read user information
group:write: Write group information
admin-access: Administrative access
securitySchemes:
oauth:
flows:
authorizationCode:
scopes:
read:users: Read user information
write:groups: Write group information
access:admin: Administrative access
Rule: Security scopes must match vendor API exactly so users know what to request. Don't normalize format.
Pattern 4: Free-Form Objects
When vendor API has truly dynamic/unknown structure objects:
badgeConfig:
type: object
description: Badge configuration for the group
badgeConfig:
type: object
properties:
color: string
description: Badge configuration for the group
Rule: Use empty type: object for truly dynamic fields from vendor API. Don't invent structure.
Summary Table
| Aspect | Rule | Example |
|---|
| allOf single ref | Use for description override | allOf: [- $ref: Schema] + description |
| Schema hierarchy | Resource/Info/Summary/Ref/ContextResource | UserInfo, OrganizationRef, GroupUser |
| Field names | Follow vendor (camelCase) | userProfilePicture not avatarUrl |
| Booleans | Follow vendor prefixes | Keep is*/has*/can* as vendor defines |
| Timestamps | Follow vendor suffixes | Keep *At/*Time/*ed as vendor defines |
| Counts | Follow vendor naming | Keep *Count/total*/num* as vendor defines |
| Path params | Follow vendor identifier type | Use userId/userName/userArn per vendor |
| Security scopes | Match vendor exactly | Don't normalize scope format |
| Free objects | Use type: object only | When vendor docs say "dynamic" |
Validation Questions
Before finalizing schemas, ask:
- allOf usage: Am I using
allOf with single ref just to add description? (✅) Or for actual composition? (Different pattern)
- Schema naming: Does this follow Resource/Info/Summary/Ref/ContextResource hierarchy?
- Field naming: Did I check the vendor API docs for the exact field name?
- Prefixes/suffixes: Am I keeping vendor's boolean/timestamp/count conventions?
- Path parameters: Am I using the identifier type (id/name/arn) that vendor uses?
- Security scopes: Do these match vendor docs exactly?
- Free objects: Did vendor docs actually say this is dynamic, or should I define structure?
Common Mistakes
❌ Mistake 1: Removing allOf "to simplify"
items:
$ref: '#/components/schemas/User'
description: Active users
❌ Mistake 2: Wrong context order
UserGroup:
properties:
groupId: string
groupName: string
GroupUser:
properties:
userId: string
userName: string
role: string
Remember: The SECOND word is the resource type being returned!
❌ Mistake 3: Normalizing vendor naming
lastLoginAt: string
❌ Mistake 4: Inventing object structure
config:
type: object
properties:
setting1: string
References
- Core Rules: api-spec-core-rules skill
- Critical 12 Rules: @.claude/rules/api-specification-critical-12-rules.md
- Schema Rules: api-spec-schemas skill