@@ -4,23 +4,17 @@ order: 4
44---
55# Validation Codes
66
7- Constraint violations returned by NeoWiki's backend validation API use stable ` code ` strings.
8- This document is the authoritative reference for NeoWiki's PHP backend validation .
7+ Constraint violations returned by NeoWiki's validation API use stable ` code ` strings. This document is
8+ the reference for those codes .
99
1010Violations are returned by:
1111
1212- ` POST /neowiki/v0/subject/validate ` — dry-run validation of a proposed create-shape body.
1313- ` POST /neowiki/v0/subject/{subjectId}/validate ` — dry-run validation of a proposed update-shape
1414 body against an existing Subject's Schema.
1515- ` POST /neowiki/v0/subject ` and ` PUT /neowiki/v0/subject/{subjectId} ` — the write endpoints include
16- the resulting ` violations ` array in their ` 201 ` /` 200 ` success body. By default
17- (` $wgNeoWikiEnforceValidation = false ` ) the write always persists. When an admin enables
18- enforcement (` $wgNeoWikiEnforceValidation = true ` ), a write that introduces * new* blocking
19- violations is rejected with ` 422 Unprocessable Entity ` and an ` { status, message, violations } `
20- body; ` violations ` carries the full proposed list, not just the newly-introduced subset.
21- Pre-existing violations (already present on the stored Subject under the current Schema) and
22- non-blocking codes such as [ ` schema-not-found ` ] ( #schema-not-found ) never block, so editing an
23- already-invalid Subject stays possible. See [ ADR 21] ( ../adr/021-add-backend-validation.md ) .
16+ the resulting ` violations ` array in their ` 201 ` /` 200 ` success body. Whether a write with violations
17+ is rejected is covered under [ Blocking and enforcement] ( #blocking-and-enforcement ) .
2418
2519The ` /validate ` endpoints return ` 200 OK ` with a ` {violations: [...]} ` body whenever the request is
2620well-formed; violations in the body do not change the HTTP status. ` 400 ` is reserved for malformed
@@ -42,218 +36,151 @@ Each violation in the response has this shape:
4236- ` propertyName ` is the property name as a string, or ` null ` for Subject-level violations.
4337- ` code ` is one of the stable strings documented below.
4438- ` args ` is always present; ` [] ` when there is nothing to interpolate.
45- - ` valuePartIndex ` is the zero-based index into the offending Value's parts. The key is ** omitted
46- from the JSON** when not applicable (Subject-level violations or property-level violations that
47- do not point at a single part).
39+ - ` valuePartIndex ` is the zero-based index of the offending part of a multi-part value. Only the
40+ codes that document it set it; the key is omitted from the JSON otherwise.
41+
42+ ## Blocking and enforcement
43+
44+ Three codes are non-blocking warnings: [ ` unregistered-type ` ] ( #unregistered-type ) ,
45+ [ ` schema-not-found ` ] ( #schema-not-found ) , and [ ` relation-target-not-found ` ] ( #relation-target-not-found ) .
46+ They are marked "Non-blocking" below and never cause a write to be rejected. Every other code blocks.
47+
48+ Blocking matters only when an admin enables enforcement (` $wgNeoWikiEnforceValidation ` ; off by
49+ default, so every write persists). Under enforcement, a write that introduces * new* blocking
50+ violations is rejected with ` 422 Unprocessable Entity ` and an ` { status, message, violations } ` body,
51+ where ` violations ` carries the full proposed list, not just the newly-introduced ones. Violations
52+ already present on the stored Subject never block, so an already-invalid Subject stays editable. See
53+ [ ADR 21] ( ../adr/021-add-backend-validation.md ) and
54+ [ ADR 26] ( ../adr/026-validation-severity-levels.md ) .
4855
4956## Code reference
5057
5158### ` required `
5259
53- Emitted by ` SubjectValidator ` (when a required property has no Statement on the Subject) and
54- by every built-in PropertyType plus ` ColorType ` (RedHerb) (when a Statement is present but its
55- Value is content-empty).
60+ A property declared ` required: true ` has no usable value. Fires when the Subject body has no
61+ Statement for the property, and when a Statement is present but its value is empty for its type:
62+ only whitespace (` text ` , ` date ` , ` dateTime ` ), no parts (` url ` , ` select ` ), no targets (` relation ` ),
63+ or no value at all (` number ` , ` boolean ` ).
64+
5665` args ` : ` [] ` .
57- ` valuePartIndex ` : never set.
58- ` propertyName ` : attached by ` SubjectValidator ` (either directly when the schema-iteration loop
59- emits, or via ` withPropertyName() ` from the per-type loop).
60-
61- Fires in two cases for any property declared ` required: true ` :
62-
63- 1 . ** No Statement at all** — the Subject body did not include the property. ` SubjectValidator `
64- iterates the Schema's properties after the per-statement loop and emits ` required ` for any
65- required property without a corresponding Statement. This also covers the case where the
66- client sent the property with an empty Value (e.g. ` value: [] ` ), because
67- ` StatementListBuilder ` drops empty-Value Statements before the validator runs, leaving the
68- schema-iteration loop to surface it.
69- 2 . ** Statement present but Value is content-empty** — emitted by the PropertyType validator.
70- What counts as "content-empty" depends on the type:
71- - ` TextType ` — no non-whitespace string in the ` StringValue ` parts.
72- - ` UrlType ` , ` SelectType ` , ` ColorType ` — ` StringValue ` has zero parts after deserialization.
73- - ` NumberType ` — the value is not a ` NumberValue ` .
74- - ` RelationType ` — the value is not a ` RelationValue ` , or has zero relations.
75- - ` DateTimeType ` — the value is not a ` StringValue ` , has zero parts, or its first part is
76- empty/whitespace.
7766
7867### ` label-required `
7968
80- Emitted by ` SubjectValidator ` only.
81- ` args ` : ` [] ` .
82- ` valuePartIndex ` : never set.
83- ` propertyName ` : always ` null ` (Subject-level).
69+ The Subject's label is empty or whitespace-only. Subject-level: ` propertyName ` is ` null ` .
8470
85- The Subject's label is empty or whitespace-only .
71+ ` args ` : ` [] ` .
8672
8773### ` type-mismatch `
8874
89- Emitted by ` SubjectValidator ` only.
90- ` args ` : ` [writerType, currentType] ` — the type recorded on the Statement when it was written
91- and the type the Schema currently declares for the property.
92- ` valuePartIndex ` : never set.
93- ` propertyName ` : the affected property.
75+ The type recorded on the Statement when it was written (ADR 11) no longer matches the type the
76+ Schema currently declares for the property — for example, a Statement written while the property was
77+ a ` url ` , after the Schema changed the property to ` number ` . When this fires, it is the only
78+ violation reported for that property: per-type checks and ` required ` are suppressed.
9479
95- The Statement's writer's-schema type (the ` propertyType ` recorded when the Statement was
96- written, per ADR 11) no longer matches the Schema's current type for that property. For example,
97- a property was originally declared as ` url ` and a Statement was written carrying a ` StringValue ` ;
98- the Schema was later edited to make that property a ` number ` . The Statement is now invalid under
99- the current Schema (ADR 12 names this case explicitly).
100-
101- ` SubjectValidator ` skips per-type validation when this fires — the per-type's ` instanceof `
102- PropertyDefinition guard would no-op against the wrong-typed definition anyway, and the more
103- specific ` type-mismatch ` code is what a consumer needs to react.
104-
105- If a property is ` required ` AND its Statement has a type mismatch, only ` type-mismatch ` fires
106- (the Statement is present, just under the wrong type). ` required ` would be redundant noise.
80+ ` args ` : ` [writerType, currentType] ` .
10781
10882### ` invalid-url `
10983
110- Emitted by ` UrlType ` .
111- ` args ` : ` [] ` .
112- ` valuePartIndex ` : index of the offending URL within the multi-part ` StringValue ` .
113- ` propertyName ` : attached by ` SubjectValidator ` .
84+ On ` url ` properties. A non-empty value does not match the allowed URL pattern. The pattern accepts
85+ ` http:// ` and ` https:// ` schemes (or no scheme), domain-like hosts, IPv4 literals, ` localhost ` ,
86+ port, path, query, and fragment. It rejects other schemes ( ` ftp:// ` , ` file:// ` ), spaces, and
87+ disallowed characters .
11488
115- A non-empty trimmed string in the ` StringValue ` does not match the allowed URL pattern. The
116- pattern accepts ` http:// ` and ` https:// ` schemes (or no scheme), domain-like hosts, IPv4
117- literals, ` localhost ` , port, path, query, and fragment. It rejects other schemes (` ftp:// ` ,
118- ` file:// ` ), spaces, and disallowed characters.
89+ ` args ` : ` [] ` . ` valuePartIndex ` : the offending part.
11990
12091### ` unique `
12192
122- Emitted by ` TextType ` and ` UrlType ` .
123- ` args ` : ` [] ` .
124- ` valuePartIndex ` : never set.
125- ` propertyName ` : attached by ` SubjectValidator ` .
93+ On ` text ` and ` url ` properties with ` uniqueItems ` enabled: the value contains duplicate parts.
12694
127- The Property's ` uniqueItems ` constraint is enabled and the ` StringValue ` contains duplicate parts .
95+ ` args ` : ` [] ` .
12896
129- ### ` min-value `
97+ ### ` min-length ` / ` max-length `
13098
131- Emitted by ` NumberType ` and ` DateTimeType ` .
132- ` args ` : ` [minimum] ` . For ` NumberType ` this is a ` number ` ; for ` DateTimeType ` this is the ISO 8601
133- string declared on the Property.
134- ` valuePartIndex ` : never set.
99+ On ` text ` properties. A part's trimmed length is below ` minLength ` or above ` maxLength ` . Empty
100+ parts are not length-checked.
135101
136- The value is strictly below the Property's inclusive minimum .
102+ ` args ` : ` [minLength] ` / ` [maxLength] ` . ` valuePartIndex ` : the offending part .
137103
138- ### ` max-value `
104+ ### ` min-value ` / ` max-value `
139105
140- Emitted by ` NumberType ` and ` DateTimeType ` .
141- ` args ` : ` [maximum] ` . Same shape as ` min-value ` .
142- ` valuePartIndex ` : never set.
106+ On ` number ` , ` date ` , and ` dateTime ` properties. The value is below the property's inclusive
107+ ` minimum ` or above its inclusive ` maximum ` .
143108
144- The value is strictly above the Property's inclusive maximum.
109+ ` args ` : ` [minimum] ` / ` [maximum] ` — a number for ` number ` properties, the declared ISO 8601 string
110+ for ` date ` and ` dateTime ` .
145111
146112### ` invalid-option `
147113
148- Emitted by ` SelectType ` and RedHerb's ` ColorType ` (when ` allowedColors ` is non-empty).
149- ` args ` : ` [offendingPart] ` .
150- ` valuePartIndex ` : index of the offending part within the ` StringValue ` .
114+ On ` select ` properties. A part is not in the property's ` options ` allow-list.
151115
152- A part is not in the Property's allow-list (` options ` for SelectType, ` allowedColors ` for
153- ColorType).
116+ ` args ` : ` [offendingPart] ` . ` valuePartIndex ` : the offending part.
154117
155118### ` single-value-only `
156119
157- Emitted by ` SelectType ` and ` RelationType ` .
158- ` args ` : ` [] ` .
159- ` valuePartIndex ` : never set (Subject-property-level, not part-level).
120+ On single-valued (` multiple: false ` ) ` select ` and ` relation ` properties: more than one part
121+ (` select ` ) or relation target (` relation ` ) was supplied.
160122
161- The Property's ` multiple ` flag is false and more than one value was supplied — more than one part for
162- ` SelectType ` , or more than one relation target for ` RelationType ` .
123+ ` args ` : ` [] ` .
163124
164125### ` invalid-datetime `
165126
166- Emitted by ` DateTimeType ` .
127+ On ` dateTime ` properties. The value is not a strict ISO 8601 / ` xsd:dateTime ` string with an
128+ explicit timezone offset or ` Z ` . Includes calendar-overflow cases like ` 2025-02-30T00:00:00Z ` ,
129+ partial dates (` 2025 ` , ` 2025-06 ` , ` 2025-06-15 ` ), and missing offsets.
130+
167131` args ` : ` [] ` .
168- ` valuePartIndex ` : never set (DateTime is single-valued).
169132
170- The non-empty first part of the ` StringValue ` is not a strict ISO 8601 / xsd: dateTime string
171- with an explicit timezone offset or ` Z ` . Includes calendar-overflow cases like
172- ` 2025-02-30T00:00:00Z ` , partial dates (` 2025 ` , ` 2025-06 ` , ` 2025-06-15 ` ), and missing offsets.
133+ ### ` invalid-date `
134+
135+ On ` date ` properties. The value is not a strict ISO 8601 calendar date (` YYYY-MM-DD ` ). Time or
136+ timezone components and calendar overflows like ` 2025-02-30 ` are rejected.
173137
174- ### ` invalid-color `
138+ ` args ` : ` [] ` .
139+
140+ ### ` unregistered-type `
175141
176- Emitted by RedHerb's ` ColorType ` .
177- ` args ` : ` [offendingPart] ` .
178- ` valuePartIndex ` : index of the offending part within the ` StringValue ` .
142+ Non-blocking. The property's type has no registered PropertyType — typically the extension providing
143+ the type is disabled. The value cannot be interpreted, so it is preserved verbatim and no other
144+ checks run for the property. A required property of an unregistered type reports this code instead
145+ of ` required ` , so the Subject stays saveable.
179146
180- A part does not match the 6-digit hex-color pattern ( ` /^#[0-9a-fA-F]{6}$/ ` ) .
147+ ` args ` : ` [propertyType] ` .
181148
182149### ` schema-not-found `
183150
184- Emitted by ` ValidateSubjectUpdateApi ` and by the Subject write endpoints
185- (` POST /subject ` , ` PUT /subject/{id} ` ) via ` ProposedSubjectValidator ` .
186- ` args ` : ` [schemaName] ` .
187- ` valuePartIndex ` : never set.
188- ` propertyName ` : always ` null ` (Subject-level).
151+ Non-blocking. The Subject's Schema cannot be loaded — usually deleted or renamed since the Subject
152+ was created, or the Subject was created or imported referencing a Schema that does not (yet) exist.
153+ The write proceeds and reports the violation; creating or renaming the Schema page resolves it.
154+ Subject-level: ` propertyName ` is ` null ` .
189155
190- The Subject's Schema cannot be loaded — usually because the Schema page was deleted or renamed
191- since the Subject was created, or because a Subject is created/imported referencing a Schema that
192- does not (yet) exist. On the write endpoints it is non-blocking: the write proceeds and the
193- (unvalidatable) Subject stays editable per ADR 21, but the response reports ` schema-not-found `
194- rather than an empty (and misleading "valid") violation list. The caller can resolve it by
195- creating or renaming the Schema page.
156+ Returned by the update dry-run and both write endpoints. The create dry-run
157+ (` POST /subject/validate ` ) instead returns ` 404 ` , because there the Schema is the addressed
158+ resource.
196159
197- Note the create dry-run endpoint (` POST /subject/validate ` ) instead returns ` 404 `
198- (` SchemaNotFoundException ` ) for a missing Schema, because there it is the addressed resource;
199- the update dry-run (` POST /subject/{id}/validate ` ) and both write endpoints surface the violation.
200- Reconciling that asymmetry is left to the enforcement tier (ADR 21).
160+ ` args ` : ` [schemaName] ` .
201161
202162### ` relation-target-schema-mismatch `
203163
204- Emitted by ` SubjectValidator ` .
205- ` args ` : ` [expectedSchema, actualSchema] ` — the target Schema the relation Property declares
206- (` targetSchema ` ), and the Schema the resolved target Subject actually uses.
207- ` valuePartIndex ` : index of the offending relation target within the ` RelationValue ` .
208- ` propertyName ` : the relation property.
164+ On ` relation ` properties. The relation targets a Subject that exists but whose own Schema is not the
165+ property's declared ` targetSchema ` . A target that cannot be resolved is reported as
166+ [ ` relation-target-not-found ` ] ( #relation-target-not-found ) instead. The subject editor filters
167+ relation candidates by target Schema, so this normally only occurs on API writes such as imports.
209168
210- A relation targets a Subject that exists but whose own Schema is not the Property's declared
211- ` targetSchema ` . The Schema compared is the target's own stored writer's-schema, read from its revision
212- slot rather than from a graph node property. Reaching that slot still resolves the target ID through
213- the subject-to-page index, which lives only in the graph projection, so this check only runs for
214- targets the graph currently knows about. This is a blocking ` error ` (ADR 26): it is rejected at write
215- time when ` $wgNeoWikiEnforceValidation ` is enabled. The subject editor's picker filters candidates by
216- target Schema, so this is normally only reachable through the API — for example a bulk import.
169+ ` args ` : ` [expectedSchema, actualSchema] ` . ` valuePartIndex ` : the offending target.
217170
218171### ` relation-target-not-found `
219172
220- Emitted by ` SubjectValidator ` .
221- ` args ` : ` [targetId] ` — the target Subject ID that did not resolve.
222- ` valuePartIndex ` : index of the offending relation target within the ` RelationValue ` .
223- ` propertyName ` : the relation property.
224-
225- A relation targets a Subject ID that does not resolve to any existing Subject. It is a non-blocking
226- ` warning ` (ADR 26), joining [ ` schema-not-found ` ] ( #schema-not-found ) and ` unregistered-type ` in the
227- hardcoded non-blocking set, so it never rejects a write even under enforcement. This is deliberate:
228- pointing at a not-yet-created Subject is wiki-native red-link behavior, and an import may legitimately
229- mint the target later.
230-
231- Resolution goes through the subject-to-page index, which lives only in the graph projection, so a
232- target that exists in revision slots but is missing from the graph is reported as not found. An
233- import populates the slots without updating the projection
234- ([ #1022 ] ( https://github.com/ProfessionalWiki/NeoWiki/issues/1022 ) ), so imported targets warn until
235- [ the graph is rebuilt] ( ../operations/maintenance.md#rebuilding-the-graph ) . Being non-blocking, this
236- misreport never costs a write.
173+ Non-blocking. On ` relation ` properties. The relation targets a Subject ID that does not resolve to
174+ any existing Subject. Deliberately a warning: pointing at a not-yet-created Subject is wiki-native
175+ red-link behavior, and an import may legitimately mint the target later.
237176
238- ## Known limitations (Foundation round)
177+ ` args ` : ` [targetId] ` . ` valuePartIndex ` : the offending target.
239178
240- The PHP ` SubjectValidator ` performs these Subject-level checks:
179+ ## Out-of-schema Statements
241180
242- - ** ` required ` ** — PHP iterates the Schema's properties to catch absent-required cases.
243- - ** ` type-mismatch ` ** — PHP compares the Statement's writer's-schema type against the Schema's
244- current type and surfaces drift per ADR 11 / ADR 12.
245- - ** [ ` relation-target-schema-mismatch ` ] ( #relation-target-schema-mismatch ) ** and
246- ** [ ` relation-target-not-found ` ] ( #relation-target-not-found ) ** — documented above; both need a
247- Subject lookup, which ` PropertyType::validate() ` has no access to.
248-
249- ### Deliberate behavior, not a gap
250-
251- - ** Out-of-schema Statements are silently skipped.** A Statement whose property is not declared
252- on the current Schema is ignored — no violation. This is schema-drift tolerance: a property
253- may have been removed from the Schema while Subjects still carry old Statements. ADR 8 says
254- one Schema per Subject, not that every Statement on that Subject must reference an extant
255- Property. If surfacing these as violations becomes useful (e.g. for migration UIs), it can be
256- added as a separate code without changing the current behavior.
181+ A Statement whose property is not declared on the current Schema is ignored — no violation. This is
182+ schema-drift tolerance: a property may have been removed from the Schema while Subjects still carry
183+ old Statements.
257184
258185## Adding a new validation code
259186
@@ -268,5 +195,7 @@ violation code:
268195 convention as the codes above.
2691964 . If the violation points at a specific part of a multi-part Value, set ` valuePartIndex ` to
270197 that part's zero-based index.
271- 5 . Document your new code in this file (or your extension's documentation if you want it to be
272- discoverable independently).
198+ 5 . Document your new code in your extension's documentation.
199+
200+ RedHerb's ` ColorType ` (` tests/RedHerb/src/ColorType.php ` ) is a worked example, including its own
201+ ` invalid-color ` code.
0 commit comments