Validation Errors & Common Rejections
A DCP message is rejected when it fails JSON Schema validation against dcp-message.schema.json, or when it fails the one cross-field rule Schema cannot express: message_type must equal <entity_type>.<verb>. This page covers the real failure modes from the DCP conformance corpus, each mapped to its SPEC rule and fix.
By InterIP Networks · Last updated 2026-07-01.
Source: the DCP SPEC and its 18-case reject corpus, current as of 2026-07-03.
The rule Schema alone can’t catch: message_type mismatch
Section titled “The rule Schema alone can’t catch: message_type mismatch”The single most important failure mode is not a Schema violation at all. Per SPEC §4.1, message_type is a denormalized convenience copy of <body.entity_type>.<body.verb> — the body is the normative source. A validator MUST independently check that the two agree; on conflict, the body wins and the message MUST be treated as malformed.
Two corpus cases exercise this:
message-type-mismatch.json—message_type: "task.updated"while the body saysentity_type: "task",verb: "created". Each field is individually valid; only the cross-check catches it.message-type-no-dot.json—message_type: "taskcreated", no.separator at all, so it can never equal<entity_type>.<verb>.
Fix: derive message_type programmatically from body.entity_type and body.verb at compose time, never set it independently. Any conformant validator — not just the bundled Ajv reference implementation — must implement this check explicitly, since plain JSON Schema 2020-12 has no cross-field equality operator for this. See the Event schema reference for the full field list this depends on.
Empty payload — an Event needs at least one of entity/delta/refs
Section titled “Empty payload — an Event needs at least one of entity/delta/refs”Per SPEC §5.1, an Event MUST contain at least one of entity, delta, or refs. A no-payload event is invalid — there’s nothing for a consumer to act on.
event-empty-payload.json shows this exactly: a body with event_id, entity_type, verb, and entity_id, but no entity, delta, or refs key at all.
Fix: every event needs a reason to exist. Use entity for a full snapshot (typically paired with a snapshot verb), delta for a change set, or refs for pure relationship assertions — and the Event schema reference documents when each is appropriate. A minimal but valid event can carry only refs, or only delta — both are accepted by the corpus (event-only-refs.json, event-only-delta.json); it’s the total absence of all three that’s rejected.
Unknown entity_type — the closed set of eight
Section titled “Unknown entity_type — the closed set of eight”entity_type is one of exactly eight values: project, task, dependency, architecture_impact, decision, review_request, finding, milestone. This is a closed set in v1 — a structural, single-responsibility guard, not an open vocabulary.
unknown-entity-type.json sends entity_type: "epic" with message_type: "epic.created". epic isn’t one of the eight nouns, so the message is rejected outright — independent of whatever verb accompanies it.
Fix: map your domain concept onto the nearest of the eight entities, or extend via a namespaced extensions object rather than inventing a new entity_type. Widening this set is a major-version concern (SPEC §10), not something a producer can do unilaterally.
Invalid identifier — pattern and length
Section titled “Invalid identifier — pattern and length”Per SPEC §3, every DCP identifier is an opaque string matching ^[a-z]+_[A-Za-z0-9._-]+$, at most 128 characters. The prefix (task_, decision_, …) is a readability convention only — it carries no routing or ownership authority.
bad-id-pattern.json sets entity_id: "Task 42" — a capital letter and a space, neither of which the pattern allows.
Fix: generate identifiers as lowercase-prefix + opaque suffix (ULID, UUID, or any stable token matching [A-Za-z0-9._-]), and keep them under 128 characters. Don’t encode meaning beyond the prefix category — consumers MUST NOT derive semantics from the rest of the string.
rel outside the closed vocabulary
Section titled “rel outside the closed vocabulary”refs entries carry a rel drawn from a closed eight-value vocabulary (SPEC §5.3): relates_to, part_of, references, caused_by, derived_from, supersedes, concerns, blocks. Routing and permission relations are intentionally excluded and MUST NOT appear, because DCP carries no trust and must not be mistaken for an authorization signal.
ref-forbidden-rel.json uses rel: "approved_by" — exactly the kind of permission-flavored relation the vocabulary excludes on purpose.
Fix: express the semantic link with the nearest closed term — often relates_to or concerns — and keep any approval/routing/assignment semantics entirely out of DCP; those belong to a transport or workflow layer. See the rel vocabulary reference for the full set and its rationale.
Unknown top-level properties — the envelope and entities are closed
Section titled “Unknown top-level properties — the envelope and entities are closed”The envelope core and every entity object are additionalProperties: false. Growth happens only through a namespaced extensions object (SPEC §9) — there is no other way to add a field without a schema change.
Two corpus cases hit this from different angles:
envelope-extra-field.json— a stray top-level"foo": "bar"next to the standard envelope fields.entity-shape-violation.json— ataskentity missing its requiredtitle, which similarly fails the entity’s strict shape.
Fix: never add ad hoc top-level keys to the envelope or an entity. If you need a custom field, put it under extensions on the envelope, the Event, or the entity — see Extensions for the full model.
Extension key not matching ^x-<vendor>$
Section titled “Extension key not matching ^x-<vendor>$”Extension keys must be lowercase and match ^x-<vendor>$ exactly (SPEC §9). This is stricter than “starts with x-”: the corpus rejects both a non-namespaced key and one with the wrong case.
non-namespaced-extension.json— key"foo", missing thex-prefix entirely.bad-extension-key.json— key"x-Vendor", capitalized, which the lowercase-only pattern rejects.
Fix: namespace every extension as x-<vendor> in all lowercase, e.g. x-tokonomix, x-acme. Each extension value is capped at 20 properties, and the extensions wrapper itself is additionalProperties: false over that pattern — nothing else is allowed to sit alongside it. Full detail in Extensions.
Bad timestamp — must be ISO-8601 UTC with a trailing Z
Section titled “Bad timestamp — must be ISO-8601 UTC with a trailing Z”Per SPEC §4.2, all DCP timestamps are ISO-8601 in UTC with a trailing Z — for example 2026-06-29T10:00:00Z. An offset form is not accepted, even though it represents the same instant.
timestamp-with-offset.json sets issued_at: "2026-06-29T10:00:00+02:00" — semantically equivalent to a UTC time, but the wrong lexical form, and rejected.
Fix: always normalize to UTC and format with a literal Z suffix before emitting — never a numeric offset, and never local time without conversion.
dcp_version must be major.minor
Section titled “dcp_version must be major.minor”dcp_version matches ^1\.[0-9]{1,4}$ — always two dot-separated integers, parsed as such and never as a float (SPEC §4).
bad-dcp-version.json sends dcp_version: "1" with no minor component, which fails the pattern.
Fix: always emit both components, e.g. "1.0", and parse consumer-side as two integers rather than a floating-point number (which would silently corrupt something like "1.10").
delta leaf not scalar/null, or too many entries
Section titled “delta leaf not scalar/null, or too many entries”Per SPEC §5.2, each delta entry is { from?, to } where to is required and leaf values MUST be scalar or null — changes to array- or object-valued fields must instead be communicated as a full snapshot via entity. delta is capped at 64 entries.
delta-nested-object.json sets a delta.meta.to value to a nested object ({ "nested": 1 }), which the scalar-or-null constraint on delta leaves rejects.
Fix: if a field’s new value isn’t a string, number, boolean, or null, don’t put it in delta — emit an entity snapshot for that entity instead (typically with verb snapshot or updated). See the Event schema reference for the full delta shape.
Missing a required envelope field
Section titled “Missing a required envelope field”Ordinary required-field checks apply too: envelope-missing-message-id.json simply omits message_id, which SPEC §4 marks as MUST. This is the most conventional kind of Schema rejection in the corpus — no cross-field logic needed, just a required property absent from the envelope. The same applies to any other MUST field across the envelope or Event — dcp_version, message_type, issued_at, body, and inside the body event_id, entity_type, verb, entity_id.
What is not an error: unknown verbs
Section titled “What is not an error: unknown verbs”It’s worth stating the contrast explicitly, because it’s easy to conflate with the closed entity_type set above: verbs are open tokens, not a closed enumeration. SPEC §7 defines a controlled vocabulary per entity type, but consumers MUST tolerate unknown verbs for forward-compatibility and pass them through unmodified.
The corpus proves this as an accept case, not a reject case: unknown-verb-tolerated.json sends message_type: "task.archived" with verb: "archived" — not in the Task-specific verb list (completed, blocked, unblocked, assigned) — and the message is accepted. The same forward-compatibility rule applies to unknown status values inside entities. Only entity_type and rel are closed, single-responsibility guards; everything else in the classification space is designed to grow without breaking older consumers. See Verbs for the full vocabulary and this open/closed distinction.
The conformance definition
Section titled “The conformance definition”Per SPEC §11, an implementation is conformant if, when validating a value as a DcpMessage, it: (1) validates against schemas/v1/dcp-message.schema.json (JSON Schema 2020-12); (2) enforces the §4.1 cross-field rule (message_type equals <entity_type>.<verb>); and (3) accepts every accept case and rejects every reject case in the conformance corpus. As of 2026-07-01 that corpus holds 25 cases — 7 accept, 18 reject — and is language-neutral: any validator, in any language, can run it to self-certify. The bundled Node/Ajv validator is one reference implementation, not the definition of conformance.
Next steps
Section titled “Next steps”Start with the envelope reference and the Event schema reference to see every field these checks apply to, then check the rel vocabulary if your rejection involves refs. For the extension mechanism referenced in several cases above, see Extensions. New to DCP entirely? The quickstart walks through composing a first valid message end to end.