Your OpenAPI spec says the endpoint accepts a userId string. But does your service
actually enforce that? Most production incidents I've seen come from schema drift—the
gateway validates one thing while the service assumes another. It's time to treat schemas as
security policy, not documentation.
TL;DR
- Treat schemas as security policy, not documentation—if it's not enforced, it's fiction
- Use two validation layers: parse/size budgets (anti-DoS) + schema validation (types/structure)
- OpenAPI 3.1 aligns with JSON Schema 2020-12—one contract, fewer mismatches
- Default to
additionalProperties: falsefor request bodies
Why This Matters More in 2026
Three trends are making schema enforcement more security-critical than ever:
- AI-augmented clients: More automated callers, more malformed inputs, more incentive for attackers to fuzz your boundaries
- Polyglot stacks: Different services use different validators with subtly different behavior
- Edge enforcement: API gateways and service mesh are common—but often configured in "log-only" mode or with stale schemas
Result: teams believe they have "validation," but the attacker hits the unvalidated path.
What Schema Validation Can (and Cannot) Do
What It's Great At
- Type safety: String vs number vs object
- Structural allowlists: Required properties,
additionalProperties: false - Constraining collections:
maxItems,uniqueItems - Budgeting inputs:
maxLength,maxProperties
What It's Not
- Authorization: Who is allowed to do it
- Stateful invariants: Rate limits, uniqueness in DB, "must match existing record"
- Canonicalization/signing: See the canonicalization article
OpenAPI 3.1 + JSON Schema: What to Use
Prefer OpenAPI 3.1 for New Work
OpenAPI 3.1 is designed to align with modern JSON Schema dialects (including 2020-12). Practically, that means:
- Fewer "OpenAPI-only" quirks
- Better tooling interoperability
- Fewer surprises when you generate validators/types from the same contract
Be Explicit About Your Dialect
In the real world, validators differ:
- Some treat unknown keywords as errors
- Some ignore them
- Some partially implement 2020-12 features like
unevaluatedProperties
The Senior-Engineer Pattern: Budgets First, Schema Second
When AppSec reviews an API boundary, I want to see this sequence:
- HTTP/body budgets: Hard limits before parsing
- Strict JSON parsing: Reject invalid JSON and ambiguous forms
- Schema validation: Fail closed; block unexpected shapes
- Semantic validation: Business rules
- Authz: Ensure the actor is allowed to request that state change
Schema validation is step 3. If you skip steps 1–2, attackers can still crash you before schema runs.
Practical Enforcement Patterns
Pattern A: Validate at Edge AND Service
Edge validation is great for:
- Dropping garbage early
- Protecting upstream capacity
- Providing consistent error formatting
But for security-critical mutations, also validate in-service because:
- Gateways get bypassed (internal paths, misroutes)
- Config drifts
- Different endpoints share the same service code
Pattern B: Validate Once, Pass the Parsed Object
If your architecture lets you parse once at the edge, validate once, and pass the validated object (or a canonical internal representation), you reduce parser divergence risk.
Schema Design That Actually Prevents Attacks
1. Default to additionalProperties: false
This is the single biggest "security posture" lever for JSON APIs:
- Catches typos (
"admni": true) - Blocks shadow fields
- Blocks proto-pollution keys in some contexts
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"userId": { "type": "string", "minLength": 1, "maxLength": 64 },
"role": { "type": "string", "enum": ["user", "admin"] }
},
"required": ["userId"],
"additionalProperties": false
} 2. Add Explicit Budget Constraints
Add to schemas (in addition to transport limits):
{
"type": "object",
"properties": {
"filters": {
"type": "array",
"items": { "type": "string", "maxLength": 64 },
"maxItems": 20
}
},
"additionalProperties": false,
"maxProperties": 8
} 3. Prefer Strings for Identifiers
This avoids:
- Integer precision issues (JS 253-1)
- Accidental float parsing
- Surprising normalization
{
"orderId": {
"type": "string",
"minLength": 1,
"maxLength": 64,
"pattern": "^[a-zA-Z0-9_-]+$"
}
} pattern if you can keep it simple.
Complex regex patterns can cause ReDoS (regex denial of service).
4. Prototype Pollution Hardening (JavaScript)
If any part of your stack merges JSON into objects, treat these keys as toxic:
__proto__constructorprototype
{
"type": "object",
"propertyNames": {
"not": { "enum": ["__proto__", "constructor", "prototype"] }
}
} Also harden runtime:
- Use safe object creation patterns (
Object.create(null)) - Avoid generic deep-merge of untrusted JSON
5. Use Discriminators for oneOf
Attackers love ambiguity. If two subschemas accept the same input, validators differ on which branch "wins."
{
"oneOf": [
{
"type": "object",
"properties": {
"kind": { "const": "email" },
"address": { "type": "string", "format": "email" }
},
"required": ["kind", "address"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"kind": { "const": "sms" },
"number": { "type": "string", "minLength": 8, "maxLength": 20 }
},
"required": ["kind", "number"],
"additionalProperties": false
}
]
} 6. Don't Allow Remote $ref at Runtime
Runtime remote reference resolution introduces:
- Supply-chain risk: Schema changes under you
- SSRF risk: Fetching refs from attacker-controlled URLs
- Availability risk: Ref fetch timeouts
$ref. Pin schema versions and validate in CI.
Where Teams Get Burned
Gateway Validates, Service Doesn't
An internal route bypasses the gateway, or a new endpoint is added without gateway config. Attackers send payloads that pass parsing but break service assumptions.
Fix: Validate in-service for mutations. Smoke-test "no-gateway" paths in CI.
Validator Config Silently Weakens Checks
Common "helpful" defaults:
- Coercing types (
"123"→123) - Removing unknown fields
- Treating formats as advisory
For security boundaries:
- Disable coercion
- Fail closed on unknown fields
- Treat formats as constraints (if your validator supports it)
Validation Error Leakage
Detailed schema errors are great for developers and also great for attackers. The compromise I like:
- Return a stable, minimal error to clients (e.g.,
INVALID_REQUESTwith a pointer) - Log the full validator errors internally with correlation ID
Implementation Checklist
- ☐ Enforce request size/depth/key/item/string budgets before schema validation
- ☐ Parse strict JSON; reject duplicate keys
- ☐ Use OpenAPI 3.1 + JSON Schema 2020-12 (or pin an explicit dialect)
- ☐ Default request schemas to
additionalProperties: false - ☐ Add
maxLength/maxItems/maxPropertiesconstraints - ☐ Avoid numeric IDs; use constrained strings
- ☐ Use discriminators for
oneOfto prevent ambiguous matches - ☐ Bundle/lock
$refat build time; disable runtime fetching - ☐ Validate at the edge and in-service for security-critical endpoints
- ☐ Keep client errors minimal; log detailed validator output internally
References
Continue Learning
- JSON Canonicalization & Signing — Stable bytes for cryptographic operations
- LLM Tool-Calling JSON Hardening — Treat AI output as untrusted
- JSON Schema Guide — Deep dive into validation
- JSON Tools — Validate schemas online