Type URI Conventions for Problem Details

This page belongs to the RFC 7807 Problem+JSON Implementation reference inside Error Contracts & Resilience Mapping. It focuses on one field that quietly determines how usable your entire error contract is: the problem type URI. Choose a scheme and a versioning discipline once, and every client can route errors deterministically; choose badly, and consumers fall back to string-matching on title or branching on status.

Under RFC 9457 (the specification that obsoletes RFC 7807 while keeping the application/problem+json media type), type is a URI reference that identifies the kind of problem. It is the primary key of your error taxonomy. Everything else — title, detail, instance — is either human prose or per-occurrence data. Only type is meant to be matched by code.

Decision trigger

You need a deliberate convention the moment either of these is true:

The fix is a stable, versioned type URI per failure, mapped one-to-one to a client exception, and reconciled against your HTTP status code mapping rather than replacing it.

URN vs HTTPS namespaces

RFC 9457 permits any URI, so the real choice is between two families.

URN namespace — an identifier with no hosting obligation:

urn:api:errors:v1:validation_failed
urn:api:errors:v1:insufficient_funds

HTTPS namespace — an identifier that also dereferences to documentation:

https://errors.api-contract.com/v1/validation-failed
https://errors.api-contract.com/v1/insufficient-funds
Property URN (urn:api:errors:…) HTTPS (https://errors…)
Dereferenceable in a browser No Yes — serve docs
Hosting / uptime obligation None You must keep the URL alive
Signals ownership via domain No Yes
Risk of clients fetching it at runtime Low Must document “do not fetch on the hot path”
Best when Internal taxonomy, no public docs Public APIs wanting self-describing errors

Either is correct. The one rule that matters more than the scheme: a given type URI must mean the same thing forever. If the semantics change, mint a new URI.

Routing a problem+json response by its type URI A problem response carrying a type URI is matched against a registry of known types and routed into a typed exception class, while an unknown type falls back to a generic error keyed on status. problem+json type: v1/insufficient status: 409 Match type URI against registry (not status) Known type InsufficientFunds Unknown type fallback by status Client error routing

Versioning and stability

A version segment (v1) is not decoration — it is the lever that lets a taxonomy evolve safely:

Treat about:blank as the deliberate default. RFC 9457 §4.2.1 says a type of about:blank means “no semantics beyond the status code,” and in that case title should be the HTTP status phrase. Use it for genuinely generic errors; upgrade to a specific URI the moment a client needs to branch.

Dereferenceable documentation (HTTPS)

If you pick HTTPS, make each type resolve to human-readable docs. A minimal contract:

# openapi/paths/errors.yaml (OpenAPI 3.1.0)
/v1/insufficient-funds:
  get:
    summary: Documentation for the insufficient-funds problem type
    responses:
      "200":
        description: Human-readable explanation and remediation steps
        content:
          text/html:
            schema: { type: string }

Document explicitly, both in prose and in your SDKs, that clients must not fetch the type URI on the request hot path — it is an identifier first, a document second.

Routing clients off type, not status

The payoff of a stable type is deterministic client routing.

TypeScript:

const REGISTRY = {
  'https://errors.api-contract.com/v1/insufficient-funds': InsufficientFundsError,
  'https://errors.api-contract.com/v1/validation-failed': ValidationError,
} as const;

export function toTypedError(problem: { type: string; status: number; detail?: string }) {
  const Ctor = REGISTRY[problem.type as keyof typeof REGISTRY];
  if (Ctor) return new Ctor(problem.detail);
  // Unknown type: fall back to a status-keyed generic error
  return new ApiError(problem.status, problem.detail);
}

Python:

REGISTRY = {
    "https://errors.api-contract.com/v1/insufficient-funds": InsufficientFundsError,
    "https://errors.api-contract.com/v1/validation-failed": ValidationError,
}

def to_typed_error(problem: dict) -> Exception:
    ctor = REGISTRY.get(problem["type"])
    if ctor:
        return ctor(problem.get("detail"))
    return ApiError(problem["status"], problem.get("detail"))  # fallback by status

Status still governs retry and caching; type governs which exception and recovery path the application takes.

Discriminator in OpenAPI

Model type as a discriminator so generators emit a typed union rather than one loose object:

components:
  schemas:
    Problem:
      oneOf:
        - $ref: '#/components/schemas/InsufficientFundsProblem'
        - $ref: '#/components/schemas/ValidationProblem'
      discriminator:
        propertyName: type
        mapping:
          "https://errors.api-contract.com/v1/insufficient-funds": '#/components/schemas/InsufficientFundsProblem'
          "https://errors.api-contract.com/v1/validation-failed": '#/components/schemas/ValidationProblem'

RFC 9457 alignment

Requirement RFC 9457 clause Convention here
type is a URI reference §3.1.1 URN or HTTPS, applied uniformly
about:blank is the default §4.2.1 Used only for status-only problems
Type SHOULD resolve to human-readable docs §3.1.1 HTTPS types serve HTML docs
Consumers key on type, not prose §3.1.1 Client registry matches type
Media type unchanged §6.1 application/problem+json

Safety and caching implications

The type URI is a public identifier, so never encode secrets, tenant IDs, or internal hostnames in it. Because a stable type is deterministic, the documentation it points at is highly cacheable (Cache-Control: max-age=86400), but the error responses that carry it are not — they hold per-request data and should be no-store. When you version a type, keep the old URI’s docs online for the deprecation window so already-shipped clients that display the link do not hit a 404.

SDK and codegen downstream effect

With type as a discriminator, generated clients narrow the union automatically:

- // no discriminator: one loose object, manual branching
- interface Problem { type: string; status: number; detail?: string }

+ // discriminated union: exhaustive, type-safe handling
+ type Problem =
+   | { type: 'https://errors.api-contract.com/v1/insufficient-funds'; status: 409; balance: number }
+   | { type: 'https://errors.api-contract.com/v1/validation-failed'; status: 422; errors: FieldError[] };

The compiler now forces a client to handle each variant, and adding a new type to the spec surfaces as a new union member the client must account for.

Common mistakes

Mistake Correct approach
Reusing a type URI after its meaning changed Mint a new versioned URI; never repurpose an existing one
Routing client logic on status alone Match on type; keep status for retry and caching
HTTPS type that returns 404 in a browser Serve dereferenceable docs at every HTTPS type URI
Omitting a version segment from the URI Include v1 so the taxonomy can evolve safely
Encoding IDs or secrets into the type path Keep type a stable, public, data-free identifier

FAQ

Should the type field be a URN or an HTTPS URL?

Both are valid URIs under RFC 9457. Use HTTPS when you want the type to be dereferenceable to human-readable docs in a browser, and a URN such as urn:api:errors:v1:validation_failed when you want an identifier with no hosting or availability obligation. The decisive rule is stability: whichever scheme you pick, a given URI must never change meaning, and you apply the same scheme across the whole taxonomy.

When should I use about:blank as the type?

Use about:blank when the problem has no semantics beyond the HTTP status code, which RFC 9457 §4.2.1 designates as the default. In that case the title should be the status phrase, such as “Not Found.” As soon as clients need to branch on a specific failure — telling one 409 apart from another — mint a dedicated type URI instead so the identifier carries the meaning.

Why route client errors on type instead of status?

The status code is coarse: many distinct failures share 400, 409, or 422. The type URI is the stable, specific identifier of the failure, so routing on type lets a client map each problem to its own typed exception and recovery path. Status still drives retry and caching decisions, but type drives application logic, which is why keeping it stable and versioned matters so much.