Skip to content

JSON Schema Validation Module

View Markdown

Status: Stable Package: dev.vertique.json.schema.validation Artifact: vertique-json-schema-validation Depends on: core, io.vertx:vertx-json-schema API reference: javadoc.io

Framework-support code for validating payloads against generated JSON Schema documents with the vertx-json-schema engine. It bounds the input that schema regular expressions may see, reports schema patterns that do not compile without echoing them, names the keyword vocabulary those steps share, and turns engine output into value-free ViolationDetail entries. It is transport-neutral and has no dependency on REST, MCP, Vert.x Web, Dagger, Micrometer, or OpenTelemetry.

Applications do not use this artifact directly. Install a framework module that validates payloads (for example vertique-rest-validation) instead. Every public type in dev.vertique.json.schema.validation is an INTERNAL framework seam — outside the application maturity promise — even though this module’s Status is Stable for the validation behavior those consumers rely on.

This module is not a schema generator, a validation engine, or a general-purpose validation DSL. It supplies the bounding, pattern-checking, and error-shaping steps around an engine that framework consumers drive themselves; they own schema generation, caching, and the response a client sees.


Do not add this dependency from application code. Use it only when authoring or extending a Vertique framework module that validates payloads against generated schemas with vertx-json-schema and must (a) bound the work a schema’s regular expressions can do on untrusted input, (b) fail fast at registration time on a schema pattern that does not compile, or (c) report violations without ever placing instance text into a message.


PatternInputGuard limits the text that regular-expression-backed checks evaluate, in UTF-16 code units:

  • Per string — a single string longer than the limit is rejected before any expression runs. The default is PatternInputGuard.DEFAULT_MAX_CHARS (4,096).
  • Per validation window — the running sum of every bounded string within one window is limited as a whole. The bound is per window, not per request: the window is whatever span the caller opens. The default is PatternInputGuard.DEFAULT_MAX_TOTAL_CHARS (262,144).

Without an open window only the per-string limit applies; the total is not counted. The constructor rejects nonsensical limits (see “PatternInputGuard” below).

Both apply at the same positions: the instance string at a pattern keyword, the property names under a non-empty patternProperties, and the instance string under the three regex-backed formats that are not measured as linear-time — idn-hostname, idn-email, and regex. A string at any other position is neither bounded nor counted. The guard also decides the formats uri, uri-reference, url, uri-template, json-pointer, relative-json-pointer, and json-pointer-uri-fragment without regular expressions, and a rejection of one of them carries a fixed message that never contains the instance.

No method in this module places instance text into a message. A violation message is built from the failed keyword and its schema arguments, or it is the fixed text "does not satisfy the schema" (ViolationDetails.VALUE_FREE_MESSAGE). A raw engine message reaches a detail only for a required violation, which names a property, never a value.

SchemaPatterns.precompile walks a schema once, at registration time, and reports every pattern and every patternProperties key that java.util.regex cannot compile. The walk never enters the value of a literal keyword (const, enum, default, examples, example), and it reads every member of properties, patternProperties, $defs, and dependentSchemas as a schema whatever its name. A patternProperties key is itself a regular expression, so a failure there, or beneath it, reports the key as [key-N] (its ordinal among that object’s keys) and never as the expression. The failure carries the JSON Pointer of the position, the JDK’s description of the syntax error, and its index — never the complete pattern. The description may quote a fragment of the pattern or of the key, so treat it as schema-author content, not as a constant.

ViolationDetails.collect renders engine output as dev.vertique.core.validation.ViolationDetail entries:

  • path — the engine’s instance location after cut-back to a declared location, in the PathForm the caller requests: URI_FRAGMENT is the engine’s own form, verbatim, including its percent-encoding (#/a%20b); RFC6901 is the JSON Pointer form (/a b), with the document root as the empty string and the member named "" as /. A missing location is the empty string. An omitted required member is reported at its parent by the engine; the path names it as <parent>/<name> (RFC 6901: the name escaped as one segment, ~ as ~0 and / as ~1, and never decoded; the fragment form: percent-encoded as the engine spells a location). The name is taken from the schema’s required array, never from the request, and is added only when the schema confirms it: the name is an entry of the required array at the unit’s keyword location, the reported location needed no cut-back, and the keyword location passes through no oneOf or not and no anyOf other than a two-entry one whose other entry is exactly {"type":"null"} (an optional value) that no $ref step precedes. Every other case, such as one of several alternatives or a parent under a key the client chose, keeps the parent location. allOf branches, $ref, then and else are named.
  • type — the violated constraint keyword (maxLength, minimum, required, …), or null when no actionable keyword exists.
  • message — a value-free message for that keyword, rendered from the schema’s own arguments.
  • args — the constraint arguments resolved from the schema (for example {maxLength: 3}), or null when there are none. Resolution reads at most 10 index digits in an array-index segment; beyond that it returns no arguments rather than guessing. The cut-back to a declared location follows at most 16 $ref hops and 64 composed subschemas, and shares the 10-digit index bound. A segment with the empty name after the first is the member "": it is kept when the schema declares it and otherwise ends the location there, so a cut-back location is shorter, never different.

A keyword that only traverses structure (properties, items, allOf, anyOf, additionalProperties, and the like — SchemaKeywords.STRUCTURAL_KEYWORDS) is a wrapper, not a violation: collect looks through wrappers to the concrete constraint, and when every error was a wrapper it returns one value-free detail instead. A violation with no usable keyword location likewise yields a value-free detail at its own instance location.

Pattern-bound rejections have their own two types, patternInputLength and patternInputTotalLength, each with path "" and args {limit: <n>}; ViolationDetails.patternInputBound maps the guard’s exceptions to them.

ViolationDetails.rfc6901 converts an engine location to RFC 6901 form: it strips one leading #, splits on /, percent-decodes each segment as UTF-8 (an invalid escape is left as it is), and keeps ~0 and ~1 untouched. #, the empty string, and null become the empty string (the document root); / and #/ stay /, the member with the empty name.

An escape the engine never writes is not honoured. The engine never percent-escapes an ASCII letter, digit or the punctuation it writes as itself (!, #, $, & to ;, =, ?, @, _, ~ and, as ~1, /), and it spells a byte below 0x10 with one hex digit, so a client key such as a control character followed by Eame is located as %6Eame. The conversion keeps such an escape as written, with the case of its hex digits, so %6Eame stays %6Eame and never becomes name; an escape beside a multi-byte run does not join it (%41%C3%A9 becomes %41é). Every other escape decodes as above. The declared-name check cuts the location back before a segment that holds such an escape, as it does for an undeclared segment, so a client key can never pass as a declared name.

This module has no configuration properties and reads no configuration source. The bounds are constructor arguments of PatternInputGuard; a consuming module decides where its own limits come from.


Compiles a schema into a Validator whose pattern positions are bounded.

  • new PatternInputGuard(maxChars, maxTotalChars) — the two limits, in UTF-16 code units; the DEFAULT_MAX_CHARS and DEFAULT_MAX_TOTAL_CHARS constants hold the defaults. The constructor throws IllegalArgumentException unless 1 <= maxChars <= maxTotalChars; its message does not contain the configured values.
  • static JsonObject rewrite(JsonObject schema) — returns a copy of the schema in which each bounded position gains one entry {"format": "x-vertique-pattern-input-bound"} appended to its allOf (BOUND_FORMAT), and each non-empty patternProperties gains a propertyNames entry carrying the same bound. Entries are appended, never prepended, so every existing allOf index and reported keyword location stays valid against the original schema. The argument is never modified.
  • Validator compile(JsonObject schema, JsonSchemaOptions options) — rewrites and compiles in one step; the guard is the compiled copy’s format validator.
  • Window openWindow() — opens the calling thread’s per-validation window; Window.close() ends it. Open one around each validation, on the guard that compiled the validator, and close it in finally. Never nest windows: opening a window while one is open on the same thread returns a no-op window, so the outer window’s total keeps running and the inner close does nothing. A window removes its own tally only, and only from the thread that opened it; closing it twice, after a newer window opened, or from another thread removes nothing else. Close a window on the thread that opened it, in a finally block: a window opened on one thread and closed on another, or never closed, leaves the opener thread’s tally in place, so later windows on that thread are no-ops and share one growing total. With no window open only the per-string limit applies.
  • PatternInputGuard.PatternInputTooLong (maxChars()) and PatternInputGuard.PatternInputTotalExceeded (maxTotalChars()) — thrown out of Validator.validate when a bound is exceeded.
PatternInputGuard guard = new PatternInputGuard(
PatternInputGuard.DEFAULT_MAX_CHARS, PatternInputGuard.DEFAULT_MAX_TOTAL_CHARS);
Validator validator = guard.compile(schema, JsonSchemaValidationOptions.draft202012());
try (PatternInputGuard.Window window = guard.openWindow()) {
OutputUnit result = validator.validate(payload);
List<ViolationDetail> details = ViolationDetails.collect(result, schema, false, PathForm.RFC6901);
} catch (PatternInputGuard.PatternInputTooLong | PatternInputGuard.PatternInputTotalExceeded e) {
ViolationDetail detail = ViolationDetails.patternInputBound(e).orElseThrow();
}

The keyword vocabulary shared by the walk, the rewrite, and the detail builder: PATTERN, PATTERN_PROPERTIES, LITERAL_KEYWORDS, NAMED_MEMBER_KEYWORDS, and STRUCTURAL_KEYWORDS, plus extractKeyword(String) (the last segment of a keyword location), isStructuralLocation(String), and constraintKeyword(OutputUnit) (the violated keyword, or null for a structural wrapper or an absent location).

draft202012() returns the JsonSchemaOptions every consumer uses to compile a Draft 2020-12 schema, so that guard-compiled validators and directly compiled ones agree on dialect and output format.

SchemaPatterns.precompile(JsonObject) returns the list of PatternCompileFailure(pointer, description, index) records described under “Pattern walk at registration time”; an empty list means every pattern compiles.

URI_FRAGMENT (the engine’s own instance-location form, verbatim) or RFC6901. The caller chooses per call to ViolationDetails.collect.

  • collect(OutputUnit result, JsonObject schema, boolean failFast, PathForm form) — the detail list described under “Violation detail shape”. With failFast true it stops after the first non-structural violation.
  • valueFree(String path) — a detail with the fixed message and null type and args.
  • safeDetail(String keyword, Map<String, Object> args, String rawMessage) — the keyword-canonical, value-free message for a violated keyword. It fails closed: a keyword whose constraint is missing from args yields <keyword> constraint violated, never the engine’s message; only required uses the engine’s message, and a null keyword yields the fixed is invalid.
  • patternInputBound(Throwable) — an Optional<ViolationDetail> for a guard exception that is the exception itself or its direct cause, empty for anything else.
  • rfc6901(String) — the conversion described above.
  • Constants VALUE_FREE_MESSAGE, PATTERN_INPUT_LENGTH_TYPE, and PATTERN_INPUT_TOTAL_LENGTH_TYPE.

None. The module declares no Dagger @Module, @Component, or injectable type; consumers construct PatternInputGuard themselves and own its lifetime.


  • vertique-core — dev.vertique.core.validation.ViolationDetail, the detail type this module produces.
  • io.vertx:vertx-json-schema — the schema engine whose Validator, OutputUnit, and JsonSchemaOptions the guard and the detail builder wrap.

Nothing else is a compile dependency; in particular the module depends on no REST module, no vertique-json, and not on vertique-json-schema.