| name | openapi-schemas |
| description | OpenAPI schema rules 14-24 for response models, nullable handling, and data modeling |
API Specification - Schemas (Rules 14-24)
Schema design, response patterns, and data modeling
Rule 14: Response Schema - Main Business Object Only
Response schemas MUST map directly to the main business object, NOT envelope/wrapper objects
responses:
'200':
schema:
type: object
properties:
status: { type: string }
data: { $ref: '#/components/schemas/User' }
meta: { type: object }
responses:
'200':
schema:
$ref: '#/components/schemas/User'
responses:
'200':
schema:
type: array
items:
$ref: '#/components/schemas/User'
WHY: Framework handles status/metadata separately. Mappers extract main object from API envelope.
Rule 15: No nullable in API Specification
🚨 CRITICAL: NEVER use nullable in api.yml
properties:
name:
type: string
nullable: true
properties:
name:
type: string
WHY: TypeScript uses | undefined for optional fields. Mappers convert null → undefined.
Rule 16: ID Fields - String Type Only
ALL ID fields MUST use type: string even if external API uses numbers
properties:
id: { type: string }
userId: { type: string }
organizationId: { type: string }
properties:
id: { type: integer }
Rationale: IDs are identifiers not quantities - prevents precision/overflow issues.
Rule 17: Path Plurality Matches Operation Type
/resources:
get:
operationId: listResources
/resources/{resourceId}:
get:
operationId: getResource
/resource:
get:
operationId: listResources
Rule 18: Clean Path Structure - Resource-Based Only
/users
/users/{userId}
/users/{userId}/repositories
/users/{userId}/repositories/{repositoryId}
/api/v1/users
/{owner}/{repo}/issues
/users/{username}/repos
Rationale: Module creates clean, consistent resource-based paths regardless of external API structure.
Rule 19: Nested Objects Must Use $ref
properties:
owner:
$ref: '#/components/schemas/User'
repository:
$ref: '#/components/schemas/Repository'
properties:
owner:
type: object
properties:
id: { type: string }
name: { type: string }
WHY: Reusability, maintainability, and type safety.
Rule 20: Schema Name Conflict Check When Adding Operations
When adding new operations, check if response schemas already exist in the spec. If YES and schemas differ:
responses:
'200':
schema:
$ref: '#/components/schemas/AccessToken'
components:
schemas:
AccessTokenDetails:
properties:
token: { type: string }
expiresIn: { type: integer }
WHY: Prevents breaking existing operations when adding new ones.
Rule 21: Never Update Connection Profile During Operation Implementation
🚨 CRITICAL: When implementing operations, NEVER modify connectionProfile.yml or connectionState.yml
# ❌ WRONG Flow
1. Implementing getAccessToken operation
2. Notice we need to store refreshToken
3. Add refreshToken to connectionState.yml # NO! STOP!
# ✅ CORRECT Flow
1. Implementing getAccessToken operation
2. Notice we need refreshToken
3. STOP and report: "connectionState needs refreshToken field"
4. User reviews and updates connection schemas
5. Resume operation implementation
WHY: Connection profile changes affect ALL operations. Must be reviewed separately.
Rule 22: Enum Fields - Keep Original API Values (Case Normalization Allowed)
CRITICAL: Enum values MUST preserve the semantic meaning from the external API. Case normalization to snake_case is allowed, but semantic transformation is forbidden.
status:
type: string
enum: [active_user, suspended_user, inactive_user]
x-enum-descriptions:
active_user: User account is active and can be used
suspended_user: User account has been suspended
inactive_user: User account is inactive
status:
type: string
enum: [A, S, I]
x-enum-descriptions:
A: Active - Account is active and can be used
S: Suspended - Account has been
[, , ]
[, , ]
[, ]
Allowed transformations:
- ✅ Case normalization:
ActiveUser → active_user
- ✅ Case normalization:
PENDING → pending
- ✅ Case normalization:
Suspended-User → suspended_user
Forbidden transformations:
- ❌ Semantic changes:
A → active (code to word)
- ❌ Value expansion:
admin → user_admin (adding prefix)
- ❌ Value contraction:
active_user → active (removing suffix)
- ❌ Synonym replacement:
inactive → disabled (different word)
WHY:
- Predictable mapping - Case normalization is mechanical and reversible
- No semantic loss - Value meaning preserved, just normalized format
- Simpler mappers -
toEnum() default snake_case transformation handles it
- x-enum-descriptions - Provides documentation without changing values
Mapper behavior:
status: toEnum(UserInfo.StatusEnum, raw.status)
status: toEnum(UserInfo.StatusEnum, raw.status)
status: toEnum(UserInfo.StatusEnum, raw.status, (apiValue: string) => {
const statusMap = { A: 'active', S: 'suspended', I: 'inactive' };
return statusMap[apiValue] || 'active';
})
Rule 23: Complete Response Model Mapping
Every response schema property must map to external API field:
User:
properties:
id:
type: string
name:
type: string
email:
type: string
createdAt:
type: string
Rule 24: Schema Context Separation - Summary vs Full
When a schema is used in BOTH nested contexts AND as direct response, consider separating:
components:
schemas:
UserSummary:
properties:
id: { type: string }
name: { type: string }
email: { type: string }
User:
allOf:
- $ref: '#/components/schemas/UserSummary'
- properties:
bio: { type: string }
location: { type: string }
/users/{userId}:
get:
responses:
'200':
schema:
$ref: '#/components/schemas/User'
/organizations/{orgId}:
get:
responses:
'200':
schema:
properties:
owner:
$ref: '#/components/schemas/UserSummary'
WHEN: Schema used in BOTH contexts AND has 10+ properties
WHY: Avoids circular references, reduces payload size for nested usage
Schema Design Checklist
Before finalizing schemas:
These schema rules ensure clean, reusable, and maintainable data models.
Schema Naming Conventions
Resource schema naming must be consistent and make sense for the resource.
Valid Naming Suffixes
- Base - Common properties shared between Resource and ResourceInfo/ResourceSummary (NOT returned by APIs)
- Info - Extended version with additional context/metadata
- Summary - Lightweight version for nested usage
- Ref - Reference with minimal fields (usually id + name)
- No suffix - The main resource schema
Configuration:
properties:
id: { type: string }
name: { type: string }
settings: { type: object }
ConfigurationBase:
properties:
id: { type: string }
name: { type: string }
ConfigurationInfo:
allOf:
- $ref: '#/components/schemas/Configuration'
- properties:
org: { $ref: '#/components/schemas/OrganizationRef' }
ConfigurationSummary:
properties:
id: { type: string }
name: { type: string }
ConfigurationDetails:
ConfigurationSettings:
ConfigurationData:
Rule: If your resource is named Configuration, then ConfigurationInfo, ConfigurationSummary, ConfigurationBase are valid. Stay consistent across the entire API.
Base Schema Usage
Base schemas:
- Contain common properties between Resource and ResourceInfo/ResourceSummary
- Should NEVER be returned by any API operation
- Used only for composition with
allOf
UserBase:
properties:
id: { type: string }
status: { type: string }
User:
allOf:
- $ref: '#/components/schemas/UserBase'
- properties:
email: { type: string }
UserInfo:
allOf:
- $ref: '#/components/schemas/UserBase'
- properties:
org: { $ref: '#/components/schemas/OrganizationRef' }
/users/{userId}:
get:
responses:
'200':
schema:
$ref: '#/components/schemas/UserBase'
Schema Design Patterns
Pattern 1: Base + Extension (Composition)
components:
schemas:
ResourceBase:
type: object
properties:
id:
type: string
format: uuid
createdAt:
type: string
format: date-time
Resource:
allOf:
- $ref: '#/components/schemas/ResourceBase'
- type: object
properties:
name:
type: string
description:
type: string
When to use: Common properties shared across multiple schemas
Pattern 2: Context Separation (Summary vs Full)
WebhookSummary:
type: object
required: [id, name, active]
properties:
id:
type: string
name:
type: string
active:
type: boolean
Webhook:
allOf:
- $ref: '#/components/schemas/WebhookSummary'
- type: object
properties:
config:
$ref: '#/components/schemas/WebhookConfig'
events:
type: array
items:
type: string
createdAt:
type: string
format: date-time
When to use: Schema used in BOTH nested and direct contexts, 10+ properties
Pattern 3: Nested Object $ref (REQUIRED)
Webhook:
type: object
properties:
config:
type: object
properties:
url: { type: string }
Webhook:
type: object
properties:
config:
$ref: '#/components/schemas/WebhookConfig'
WebhookConfig:
type: object
properties:
url:
type: string
format: uri
Always required: ALL nested objects MUST use $ref
Pattern 4: Format Application
properties:
id:
type: string
format: uuid
createdAt:
type: string
format: date-time
website:
type: string
format: uri
email:
type: string
format: email
See type-mapping skill for complete format reference.
Schema Validation Scripts
Check for Inline Schemas in Responses
yq eval '.paths.*.*.responses.*.content.*.schema | select(has("properties"))' api.yml
Check All Nested Objects Use $ref
grep -A 5 "type: object" api.yml | grep -B 2 "properties:" | grep -v "\$ref"
Check for Nullable Usage (FORBIDDEN)
grep "nullable:" api.yml
Check Format Application
yq eval '.. | select(has("format")) | .format' api.yml
Check Schema Context Separation
grep -r "ref.*schemas/User" api.yml
Schema Design Workflow
- Identify unique objects in API responses
- Create base schemas for common properties
- Use allOf for composition
- Never duplicate properties across schemas
- Apply formats consistently (uuid, date-time, uri, email)
- Separate schemas by context (Summary vs Full when needed)
- Validate no inline schemas remain
- Document external API mapping for each property
Validation Checklist
Run these checks before finalizing schemas:
cd package/{vendor}/{service}
yq eval '.paths.*.*.responses.*.content.*.schema | select(has("properties"))' api.yml
grep "nullable:" api.yml
yq eval '..| select(.properties.id) | .properties.id.type' api.yml
grep -A 5 "type: object" api.yml | grep -B 2 "properties:" | grep -v "\$ref"
yq eval '.. | select(has("format")) | .format' api.yml | sort | uniq
Success Metrics
Schema design is complete when:
- ✅ Zero inline schemas in responses
- ✅ No duplicate schema definitions
- ✅ Proper format application (uuid, date-time, uri, email)
- ✅ Clean composition with allOf
- ✅ Context-appropriate schemas (Summary vs Full where needed)
- ✅ All nested objects use $ref
- ✅ No nullable in spec (null → undefined happens in mappers)
- ✅ All IDs use type: string
- ✅ Clean resource-based paths
- ✅ External API mapping documented