name: mapper-validation description: Mapper field validation process ensuring zero missing fields
Mapper Field Validation Process
Complete validation workflow for ensuring mappers correctly handle all interface fields.
MANDATORY 3-STEP VALIDATION
Every mapper MUST follow this validation process:
Step 1: Interface Analysis
READ the target interface from generated/api/index.ts:
// Example: After running npm run clean && npm run generate
interface UserInfo {
id: UUID; // Required
name: string; // Required
email?: Email; // Optional
createdAt: Date; // Required
status?: StatusEnum;// Optional
}
DOCUMENT ALL FIELDS:
- Count total fields: 5
- Required fields (3): id, name, createdAt
- Optional fields (2): email, status
- CRITICAL: Record the exact count
Step 2: API Response Schema Validation
EXAMINE the corresponding response schema in api.yml:
components:
schemas:
UserInfo:
type: object
required:
- id
- name
- created_at
properties:
id:
type: string
name:
type: string
email:
type: string
format: email
created_at:
type: string
format: date-time
status:
type: string
enum: [active, inactive, pending]
VERIFY:
- Each interface field has corresponding API response field
- Required fields match between interface and schema
- Optional fields are correctly optional
Step 3: Complete Mapping Implementation
IMPLEMENT mapper with ALL fields using any:
export function toUserInfo(raw: any): UserInfo {
// Validate required fields first using ensureProperties
ensureProperties(raw, ['id', 'name', 'created_at']);
// 🚨 CRITICAL: Map ALL fields (must match interface count)
const output: UserInfo = {
// Required (3) - validated above
id: String(raw.id),
name: raw.name,
createdAt: map(DateTime, raw.created_at),
// Optional (2) - MUST be mapped too
email: map(Email, raw.email),
status: toEnum(StatusEnum, raw.status)
};
return output;
}
Type Safety Rules:
- ✅ ALL mapper parameters use
anyfor simplicity - ✅ Use
ensureProperties()helper for validating required fields - ✅ Add
/* eslint-disable */at top of mapper file
**VALIDATION**:
- **ZERO MISSING FIELDS**: Interface field count = Mapped field count
- If interface has 5 fields → mapper MUST map 5 fields
- If interface has 10 fields → mapper MUST map 10 fields
## Field Count Validation
**MANDATORY CHECK**: Before considering mapper complete, count fields:
```typescript
// ✅ CORRECT - All 5 fields mapped
interface UserInfo {
id: UUID; // 1
name: string; // 2
email?: Email; // 3
createdAt: Date; // 4
status?: StatusEnum;// 5
}
export function toUserInfo(raw: any): UserInfo {
ensureProperties(raw, ['id', 'name', 'created_at']);
const output: UserInfo = {
id: map(UUID, raw.id), // 1 ✓
name: raw.name, // 2 ✓
email: map(Email, raw.email), // 3 ✓
createdAt: map(DateTime, raw.created_at), // 4 ✓
status: toEnum(StatusEnum, raw.status) // 5 ✓
};
return output;
}
// Field count: 5 = 5 ✅ PASS
// ❌ WRONG - Missing fields
export function toUserInfo(raw: any): UserInfo {
ensureProperties(raw, ['id', 'name', 'created_at']);
const output: UserInfo = {
id: map(UUID, raw.id), // 1 ✓
name: raw.name, // 2 ✓
createdAt: map(DateTime, raw.created_at) // 3 ✓
// Missing: email (field 4)
// Missing: status (field 5)
};
return output;
}
// Field count: 3 ≠ 5 ❌ FAIL
RULE: Interface field count MUST EQUAL mapped field count.
Nested Object Handling
For nested objects, use non-exported helper mappers:
// Parent interface
interface Organization {
id: UUID;
name: string;
address?: Address; // Nested object
}
// Nested interface
interface Address {
street: string;
city: string;
country?: string;
}
// ✅ CORRECT - Helper mapper for nested object with validation
function toAddress(raw: any): Address {
// 🚨 CRITICAL: Helper functions MUST validate required fields
ensureProperties(raw, ['street', 'city']);
const output: Address = {
street: raw.street,
city: raw.city,
country: optional(raw.country)
};
return output;
}
// Main mapper uses helper with mapWith
export function toOrganization(raw: any): Organization {
ensureProperties(raw, ['id', 'name']);
const output: Organization = {
id: String(raw.id),
name: raw.name,
address: mapWith(toAddress, raw.address) // mapWith handles null/undefined
};
return output;
}
CRITICAL RULES:
- ✅ Helper mappers MUST use
anyparameter type - ✅ Helper mappers MUST validate their own required fields with
ensureProperties() - ✅ Helper mappers are NOT exported (no
exportkeyword) - ✅ Defined above the main mapper function
- ✅ Use
mapWith()in parent to handle null/undefined at boundary - ✅ Helper returns plain type
T, notT | undefined(mapWith adds the undefined) - ❌ NEVER skip validation in helper functions
Optional Field Handling
Optional fields MUST still be mapped, returning undefined when absent:
// ✅ CORRECT - Optional field mapped
export function toUser(data: any): User {
ensureProperties(data, ['id', 'name']);
const output: User = {
id: map(UUID, data.id), // Required
name: data.name, // Required
email: map(Email, data.email), // Optional - returns Email | undefined
website: map(URL, data.website) // Optional - returns URL | undefined
};
return output;
}
// ❌ WRONG - Optional field missing
export function toUser(data: any): User {
ensureProperties(data, ['id', 'name']);
const output: User = {
id: map(UUID, data.id),
name: data.name,
email: map(Email, data.email)
// Missing: website - even though optional, must be mapped
};
return output;
}
WHY: TypeScript interface includes the field, mapper must provide it (even if undefined).
Validation Checklist
Before completing a mapper:
- Read generated interface from
generated/api/index.ts - Counted total interface fields (required + optional)
- Documented required vs optional fields
- Checked API response schema in api.yml
- Validated required fields with error throwing
- Mapped ALL required fields
- Mapped ALL optional fields (with
| undefined) - Used
const output: Typepattern - Verified field count: interface count = mapper count
- No fields missing from mapper
- Helper mappers for nested objects (not exported)
Common Mistakes
Mistake 1: Skipping Optional Fields
// ❌ WRONG - Skipping optional fields
interface User {
id: UUID;
name: string;
email?: Email; // Optional but must be mapped!
phone?: string; // Optional but must be mapped!
}
export function toUser(data: any): User {
ensureProperties(data, ['id', 'name']);
return {
id: map(UUID, data.id),
name: data.name
// Missing email and phone - WRONG even if optional
};
}
FIX: Map all fields, optional or not.
Mistake 2: Not Counting Fields
// ❌ WRONG - No validation of field count
// Developer maps 3 fields, interface has 5, doesn't notice
FIX: Explicitly count before and after.
Mistake 3: Assuming Defaults
// ❌ WRONG - Assuming undefined for missing fields
interface Resource {
id: UUID;
name: string;
tags?: string[]; // Optional array
}
export function toResource(data: any): Resource {
ensureProperties(data, ['id', 'name']);
return {
id: map(UUID, data.id),
name: data.name
// Missing tags - TypeScript might allow but it's wrong
};
}
FIX: Explicitly map optional fields:
export function toResource(data: any): Resource {
ensureProperties(data, ['id', 'name']);
return {
id: map(UUID, data.id),
name: data.name,
tags: data.tags || undefined // Explicit
};
}
Nested Model Example
Complete example with nested objects:
// Interfaces (from generated/api/index.ts)
interface Organization {
id: UUID;
name: string;
address?: Address;
owner?: User;
}
interface Address {
street: string;
city: string;
postalCode?: string;
}
interface User {
id: UUID;
name: string;
email?: Email;
}
// Helper mappers (not exported)
function toAddress(data: any): Address {
ensureProperties(data, ['street', 'city']);
const output: Address = {
street: data.street,
city: data.city,
postalCode: data.postal_code || undefined // All 3 fields mapped
};
return output;
}
function toUser(data: any): User {
ensureProperties(data, ['id', 'name']);
const output: User = {
id: map(UUID, data.id),
name: data.name,
email: map(Email, data.email) // All 3 fields mapped
};
return output;
}
// Main mapper (exported)
export function toOrganization(data: any): Organization {
ensureProperties(data, ['id', 'name']);
const output: Organization = {
id: map(UUID, data.id),
name: data.name,
address: mapWith(toAddress, data.address), // mapWith handles null/undefined
owner: mapWith(toUser, data.owner) // mapWith handles null/undefined
};
return output; // All 4 fields mapped
}
VALIDATION:
- Organization interface: 4 fields → Mapper: 4 fields ✅
- Address interface: 3 fields → Helper mapper: 3 fields ✅
- User interface: 3 fields → Helper mapper: 3 fields ✅
Discovery Process
When implementing a new mapper:
-
Run generation:
npm run clean && npm run generate -
Open generated interface:
// generated/api/index.ts export interface NewResource { // Read all fields here } -
Count fields:
- Total: ?
- Required: ?
- Optional: ?
-
Check API schema:
# api.yml components: schemas: NewResource: required: [...] properties: ... -
Implement mapper:
export function toNewResource(data: any): NewResource { ensureProperties(data, ['required_field1', 'required_field2']); const output: NewResource = { // Map ALL fields }; return output; } -
Validate:
- Count fields in mapper
- Compare with interface count
- Must match exactly
Runtime Validation (MANDATORY)
CRITICAL: Static field counting is necessary but NOT sufficient.
After implementing mapper, you MUST perform runtime validation to detect:
- Field name mismatches (API
mobilePhonevs specphoneNumber) - Fields in API but not in spec
- Fields in spec but not mapped correctly
- Type conversion errors
When to Run Runtime Validation
MANDATORY for:
- New module creation (after all operations implemented)
- Adding new operations to existing module
- API spec updates
- After mapper refactoring
Quick Runtime Validation Process
-
Add temporary debug log in producer BEFORE mapping:
this.logger.debug('[TEMP-RAW-API] operationName:', JSON.stringify(response.data, null, 2)); -
Run tests with debug logging:
env LOG_LEVEL=debug npm run test:integration > /tmp/debug.log 2>&1 -
Compare raw API vs mapped output:
# Extract raw API response grep -A 100 "\[TEMP-RAW-API\]" /tmp/debug.log # Compare with mapped result in test logs grep "Api\." /tmp/debug.log -
Fix any missing fields then remove temp logs
RULE: Mapper validation is NOT complete until runtime validation shows ZERO missing fields.
See complete process: mapper-runtime-validation skill
References
- Mapper Pattern: implementation skill (Mapper Pattern section)
- Runtime Validation: mapper-runtime-validation skill
- Type Mapping: type-mapping skill
- Error Handling: error-handling skill
- Operation Engineer agent: @.claude/agents/operation-engineer.md
- Mapping Engineer agent: @.claude/agents/mapping-engineer.md