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:

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.