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:
- Two different failures return the same status (a
409for a duplicate email and a409for a stale ETag), and a client must tell them apart to recover correctly. - A client is
switch-ing on thetitlestring or a regex overdetail— a sign the stable identifier is missing and prose has become load-bearing.
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.
Versioning and stability
A version segment (v1) is not decoration — it is the lever that lets a taxonomy evolve safely:
- Keep the same URI for backwards-compatible clarifications (better
title, richer docs). The identity of the failure has not changed. - Mint a new URI (
…/v2/validation-failed, or a new URN version segment) when the meaning changes — for example, when a singlevalidation_failedsplits intoschema_invalidandbusiness_rule_violated. Never repurpose an existing URI, because clients keyed on it will silently misroute.
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.
Related
- RFC 7807 Problem+JSON Implementation — up-link: the end-to-end reference this convention supports
- Error Contracts & Resilience Mapping — up-link: the parent reference for error envelopes and resilience mapping
- Adding Extension Members to Problem Details — the members you attach once
typeanchors the payload - Building a Machine-Readable Error Registry — one source of truth for every
typeURI and its title - HTTP Status Code Mapping — how the status that accompanies each
typeis chosen