Claude-skill-registry-data mapper-validation

Mapper field validation process ensuring zero missing fields

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry-data
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry-data "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/mapper-validation" ~/.claude/skills/majiayu000-claude-skill-registry-data-mapper-validation && rm -rf "$T"
manifest: data/mapper-validation/SKILL.md
source content

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
    any
    for 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
    any
    parameter type
  • ✅ Helper mappers MUST validate their own required fields with
    ensureProperties()
  • ✅ Helper mappers are NOT exported (no
    export
    keyword)
  • ✅ Defined above the main mapper function
  • ✅ Use
    mapWith()
    in parent to handle null/undefined at boundary
  • ✅ Helper returns plain type
    T
    , not
    T | 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: Type
    pattern
  • 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:

  1. Run generation:

    npm run clean && npm run generate
    
  2. Open generated interface:

    // generated/api/index.ts
    export interface NewResource {
      // Read all fields here
    }
    
  3. Count fields:

    • Total: ?
    • Required: ?
    • Optional: ?
  4. Check API schema:

    # api.yml
    components:
      schemas:
        NewResource:
          required: [...]
          properties:
            ...
    
  5. Implement mapper:

    export function toNewResource(data: any): NewResource {
      ensureProperties(data, ['required_field1', 'required_field2']);
    
      const output: NewResource = {
        // Map ALL fields
      };
      return output;
    }
    
  6. 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
    mobilePhone
    vs spec
    phoneNumber
    )
  • 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

  1. Add temporary debug log in producer BEFORE mapping:

    this.logger.debug('[TEMP-RAW-API] operationName:', JSON.stringify(response.data, null, 2));
    
  2. Run tests with debug logging:

    env LOG_LEVEL=debug npm run test:integration > /tmp/debug.log 2>&1
    
  3. 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
    
  4. 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