Skip to content
Back to skills

Response Schema Validation

ASecurity

Use when building APIs to enforce that every response matches a declared schema before reaching the client. Prevents accidental data leakage, field mismatches, and contract violations.

  • 7 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 27, 2026
developmenttypescriptpythongoapidatabasefrontendbackendsecurityperformance

Works with

  • cli
  • api

Security analysis

A100/100

Scanned May 27, 2026

npx -y skills add Vimalk0703/shipworthy --skill response-schema-validation --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Response Schema Validation?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Response Schema Validation
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vimalk0703-response-schema-validation/badge)](https://www.skillsdirectory.com/skills/vimalk0703-response-schema-validation)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: response-schema-validation
description: Use when building APIs to enforce that every response matches a declared schema before reaching the client. Prevents accidental data leakage, field mismatches, and contract violations.
invoke_when: Use when writing API endpoints, response handlers, serializers, or any code that returns data to clients. Also invoke during code review of API routes.
---

# Response Schema Validation

## Core Rule

**Every API response must be validated against a declared schema before it leaves the server.** Returning raw database objects or unvalidated data is a data leakage vector and a contract violation.

## Why This Matters

| Risk | What Happens Without Schema Validation |
|------|---------------------------------------|
| Data leakage | Internal fields (`password_hash`, `internal_notes`, `admin_flag`) accidentally returned to clients |
| Contract drift | Frontend expects `{ user_id }` but backend returns `{ userId }` after a refactor — no error, silent failure |
| Over-fetching | Entire database rows returned when the client needs 3 fields — bandwidth waste and attack surface expansion |
| Type mismatches | `null` where a string is expected, number-as-string, missing required fields — client crashes |

## Implementation Patterns

### TypeScript (Zod)

Define response schemas alongside request schemas. Validate before sending:

```typescript
// BAD — returning raw database row (leaks internal fields)
app.get('/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
  res.json(user); // exposes passwordHash, internalNotes, loginAttempts
});
```

```typescript
// GOOD — validate through a declared response schema
// schemas/user.ts
import { z } from 'zod';

export const UserResponse = z.object({
  id: z.string().uuid(),
  name: z.string(),
  email: z.string().email(),
  role: z.enum(['user', 'admin']),
  createdAt: z.string().datetime(),
});

// NEVER include: passwordHash, internalNotes, loginAttempts
export type UserResponse = z.infer<typeof UserResponse>;
```

```typescript
// routes/users.ts
import { UserResponse } from '../schemas/user';

app.get('/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
  if (!user) return res.status(404).json({ error: 'Not found' });

  // Validate and strip — only declared fields pass through
  const response = UserResponse.parse(user);
  res.json(response);
});
```

### Python (Pydantic)

```python
from pydantic import BaseModel
from datetime import datetime

class UserResponse(BaseModel):
    id: str
    name: str
    email: str
    role: str
    created_at: datetime

    class Config:
        # Strip undeclared fields — defense against leakage
        extra = "forbid"

@app.get("/users/{user_id}")
async def get_user(user_id: str) -> UserResponse:
    user = await db.users.find_by_id(user_id)
    # Pydantic validates and strips on construction
    return UserResponse.model_validate(user)
```

### Go

```go
type UserResponse struct {
    ID        string `json:"id"`
    Name      string `json:"name"`
    Email     string `json:"email"`
    Role      string `json:"role"`
    CreatedAt string `json:"created_at"`
    // No unexported fields leak — Go handles this by default
    // But explicitly map DB model -> response model
}

func toUserResponse(u *db.User) UserResponse {
    return UserResponse{
        ID:        u.ID,
        Name:      u.Name,
        Email:     u.Email,
        Role:      u.Role,
        CreatedAt: u.CreatedAt.Format(time.RFC3339),
    }
}
```

## Response Schema Rules

1. **Declare a response schema for every endpoint** — no exceptions. If an endpoint returns data, it has a schema.
2. **Response schemas are allowlists** — only declared fields pass through. Configure `strict` mode (Zod) or `extra = "forbid"` (Pydantic).
3. **Never return raw database models** — always map through a response schema. Database models have internal fields that must not leak.
4. **Separate schemas for list vs. detail** — `UserListItem` (id, name) vs. `UserDetail` (id, name, email, role, createdAt). List endpoints return less data.
5. **Version your response schemas** — when API versions change, the response schema changes. Old clients get old schemas.
6. **Validate in tests** — write tests that assert response bodies match the declared schema. Catch drift before deployment.

## Sensitive Fields Blocklist

These fields must NEVER appear in any response schema:

| Field Pattern | Why |
|--------------|-----|
| `*password*`, `*hash*`, `*salt*` | Authentication credentials |
| `*secret*`, `*token*` (non-public) | API keys and session tokens |
| `*internal*`, `*admin_notes*` | Internal-only data |
| `*ssn*`, `*tax_id*` | Government identifiers |
| `*credit_card*`, `*cvv*`, `*pan*` | Payment card data |
| `login_attempts`, `failed_auth_count` | Security metadata attackers can exploit |

If any of these patterns appear in a response schema, flag it immediately.

## Test Pattern

```typescript
// test/api/users.test.ts
import { UserResponse } from '../schemas/user';

it('GET /users/:id returns only declared fields', async () => {
  const res = await request(app).get('/users/1');

  // Schema validation — will throw if extra fields present
  const parsed = UserResponse.strict().parse(res.body);

  // Explicitly verify no sensitive fields leaked
  expect(res.body).not.toHaveProperty('passwordHash');
  expect(res.body).not.toHaveProperty('internalNotes');

  // Verify all expected fields present
  expect(parsed.id).toBeDefined();
  expect(parsed.email).toBeDefined();
});
```

## Rationalization Pressure Test

| Excuse | Counter |
|--------|---------|
| "The frontend only uses the fields it needs" | The frontend ignoring extra fields does not prevent them from being sent. An attacker reads the raw response, not the UI |
| "We control both client and server" | Today. Tomorrow a mobile app, a third-party integration, or an API consumer you did not build will hit this endpoint |
| "Adding response schemas is boilerplate" | A 5-line Zod schema is less work than a data breach investigation. The boilerplate is the protection |
| "Our ORM already selects specific fields" | ORM select lists are in a query file far from the route handler. A response schema is the last line of defense at the boundary |
| "It's just an internal API" | Internal APIs get exposed through SSRF, misconfigured proxies, and lateral movement. Internal does not mean unexploitable |
| "Performance — parsing responses adds overhead" | Schema validation adds microseconds. Sending entire database rows adds kilobytes of unnecessary data per request |

## Code Review Checklist

- [ ] Every endpoint has a declared response schema
- [ ] Response schemas use strict/forbid mode (no extra fields pass)
- [ ] No raw database models returned directly
- [ ] Sensitive field blocklist checked against response schemas
- [ ] List endpoints return fewer fields than detail endpoints
- [ ] Response schema validated in at least one test per endpoint
- [ ] Error responses also have schemas (consistent error format)

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…