RFC 7807 Problem+JSON Implementation: Spec Validation & Client Generation Workflows
Standardizing error contracts across distributed systems requires strict adherence to RFC 7807 (application/problem+json). This guide provides implementation workflows for backend engineers, API architects, and platform teams to enforce schema compliance in CI/CD, generate type-safe SDKs, and integrate error payloads into resilience and observability pipelines.
1. RFC 7807 Specification Fundamentals
RFC 7807 mandates five core fields: type, title, status, detail, and instance. Establishing a baseline schema ensures cross-service consistency and aligns with broader Error Contracts & Resilience Mapping strategies.
Minimal Payload Structure
{
"type": "https://api.example.com/errors/v1/validation-failed",
"title": "Validation Error",
"status": 400,
"detail": "Request body contains invalid field types.",
"instance": "/api/v1/users"
}
OpenAPI 3.1 Schema Definition
Define ProblemDetail using oneOf and discriminator to enable strict validation and downstream SDK generation.
components:
schemas:
ProblemDetail:
type: object
required: [type, title, status]
properties:
type:
type: string
format: uri
title:
type: string
status:
type: integer
minimum: 100
maximum: 599
detail:
type: string
instance:
type: string
format: uri-reference
errors:
type: array
items:
type: object
properties:
field: { type: string }
code: { type: string }
message: { type: string }
discriminator:
propertyName: type
mapping:
https://api.example.com/errors/v1/validation-failed: '#/components/schemas/ValidationErrorDetail'
Pre-Commit Validation Hook
Enforce JSON Schema draft compliance before commits:
# .husky/pre-commit
npx ajv-cli validate -s ./schemas/problem-detail.schema.json -d ./responses/*.json --strict=true
2. HTTP Status Code Alignment & Mapping
The status field must strictly mirror the HTTP response code. Framework-level interceptors should enforce this invariant to prevent contract drift. Refer to HTTP Status Code Mapping for routing guidelines.
Express.js Middleware Interceptor
import { Request, Response, NextFunction } from 'express';
export function problemJsonInterceptor(err: any, _req: Request, res: Response, _next: NextFunction) {
const statusCode = err.statusCode || 500;
res.status(statusCode).json({
type: err.type || `urn:api:errors:internal:${statusCode}`,
title: err.title || 'Internal Server Error',
status: statusCode, // MUST match res.statusCode
detail: process.env.NODE_ENV === 'production' ? 'An unexpected error occurred.' : err.message,
instance: req.originalUrl,
trace_id: res.getHeader('x-request-id') as string
});
}
Contract Test Assertion
// Jest / Supertest
expect(response.status).toBe(404);
expect(response.body.status).toBe(404); // Strict equality enforcement
expect(response.headers['content-type']).toContain('application/problem+json');
3. CI/CD Pipeline Integration & Contract Testing
Automate schema validation, mock server generation, and regression testing to block breaking changes before deployment.
GitHub Actions Workflow
name: API Contract Validation
on: [pull_request]
jobs:
validate-spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate OpenAPI Spec
run: npx @redocly/cli lint openapi.yaml
- name: Generate Mock Server
run: npx @stoplight/prism-cli mock openapi.yaml --dynamic --port 4010 &
- name: Run Contract Tests
run: npx schemathesis run http://localhost:4010 --checks all --hypothesis-seed=1
- name: Pact Verification (Optional)
run: npx pact-provider-verifier --pact-broker-base-url=https://broker.example.com --provider-base-url=http://localhost:4010
Key CI Gates:
openapi-generatorvalidation step to ensurediscriminatormappings resolve.- Schema drift detection via
diffagainst baselineopenapi.yaml. - Block PRs if
statusfield type deviates fromintegeror required fields are missing.
4. Type-Safe Client Generation Workflows
Leverage OpenAPI specs to generate SDKs with discriminated unions. This eliminates runtime instanceof checks and enables compile-time error narrowing.
OpenAPI Generator CLI
openapi-generator-cli generate \
-i openapi.yaml \
-g typescript-fetch \
-o ./clients/ts-sdk \
--additional-properties=useSingleRequestParameter=true,supportsES6=true \
--type-mappings=ProblemDetail=ProblemDetail
TypeScript fetch Wrapper with Discriminated Union Parsing
type ProblemDetail = {
type: string;
title: string;
status: number;
detail?: string;
instance?: string;
};
export async function safeFetch<T>(url: string, init?: RequestInit): Promise<T | ProblemDetail> {
const res = await fetch(url, init);
if (!res.ok) {
const payload = await res.json() as ProblemDetail;
// Type narrowing based on status/type
if (payload.status === 400) throw new ValidationError(payload);
if (payload.status === 429) throw new RateLimitError(payload);
throw new GenericApiError(payload);
}
return res.json() as Promise<T>;
}
Runtime Validation (Zod / Pydantic)
import { z } from 'zod';
export const ProblemDetailSchema = z.object({
type: z.string().url(),
title: z.string(),
status: z.number().int().min(100).max(599),
detail: z.string().optional(),
instance: z.string().url().optional(),
});
// Enforce strict payload parsing at gateway/edge layer
const validated = ProblemDetailSchema.parse(rawResponse);
Custom Axios Interceptor
axios.interceptors.response.use(
(res) => res,
(error) => {
const problem = error.response?.data as ProblemDetail;
if (problem?.type) {
const TypedError = ErrorRegistry.get(problem.type) || UnknownApiError;
return Promise.reject(new TypedError(problem));
}
return Promise.reject(error);
}
);
5. Resilience & Retry Logic Integration
Parse type URIs and status codes to drive automated retry policies and circuit breakers. Classification aligns with Retryable vs Non-Retryable Errors guidelines.
Resilience4j / Polly Configuration
# Resilience4j retry config
resilience4j:
retry:
instances:
api-client:
max-attempts: 3
wait-duration: 500ms
retry-exceptions:
- com.example.errors.RetryableProblemException
ignore-exceptions:
- com.example.errors.NonRetryableProblemException
Custom Error Parser (Go)
func IsRetryable(problem ProblemDetail) bool {
switch problem.Type {
case "urn:api:errors:v1:rate-limited", "urn:api:errors:v1:service-unavailable":
return true
case "urn:api:errors:v1:validation-failed", "urn:api:errors:v1:not-found":
return false
default:
return problem.Status >= 500 && problem.Status != 501
}
}
Telemetry Tagging
Attach problem.type and problem.status to metrics for alerting:
metrics.increment('api.errors', 1, {
type: problem.type,
status: String(problem.status),
service: 'user-api',
trace_id: req.headers['x-request-id']
});
6. Microservice Standardization & Observability
Deploy centralized error middleware and structured logging to unify traceability across service boundaries. Implementation follows Standardizing error responses across microservices best practices.
OpenTelemetry Span Enrichment
from opentelemetry import trace
def enrich_span_with_problem(span: trace.Span, problem: dict):
span.set_attribute("error.type", problem.get("type"))
span.set_attribute("error.status", problem.get("status"))
span.set_attribute("error.instance", problem.get("instance"))
span.set_status(trace.StatusCode.ERROR, problem.get("title"))
Log Aggregation Filters (Fluent Bit / Datadog)
# Fluent Bit Parser
[PARSER]
Name problem_json
Format json
Time_Key timestamp
Time_Format %Y-%m-%dT%H:%M:%S.%L
Types status:integer
# Filter to route high-severity errors
[FILTER]
Name grep
Match service.*
Regex status ^(5[0-9]{2})$
Anti-Patterns & Validation Checks
| Pitfall | Remediation |
|---|---|
Mismatched status field vs actual HTTP status code |
Implement middleware assertions: assert payload.status === res.statusCode |
Omitting type URI or using unstable URLs |
Use versioned URNs/URIs: urn:api:errors:v2:auth:token_expired |
Overloading detail with stack traces/PII |
Strip stack traces in production; use trace_id for debugging |
Failing to version error type URIs |
Append v1, v2 to URI paths or URN namespaces |
Ignoring instance for request correlation |
Always inject req.originalUrl or req.path into instance |
| Generating clients without discriminated unions | Configure OpenAPI discriminator on type and enable union generation flags |
FAQ
Should the type field be a URI or a simple string?
RFC 7807 specifies a URI. Use a stable, versioned namespace (e.g., urn:api:errors:v1:validation_failed) for machine readability and future-proofing.
How should validation errors be structured in RFC 7807?
Use an errors extension array containing objects with field, code, and message properties, keeping detail for a high-level summary.
Can Problem+JSON coexist with GraphQL error formats?
Yes. Map GraphQL extensions to RFC 7807 fields at the HTTP transport layer or use a custom extensions object within the Problem payload.
How do I enforce RFC 7807 compliance in CI/CD?
Use JSON Schema validation against OpenAPI definitions, run contract tests on mock endpoints, and block PRs on schema drift.