# JSON Schema Validation Module

[View HTML](https://vertique.dev/docs/modules/vertique-json-schema-validation/)

Documentation version: 0.3.0

<!--
SPDX-FileCopyrightText: 2026 Koivisto Capital Oy
SPDX-License-Identifier: EUPL-1.2
-->

> **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](https://javadoc.io/doc/dev.vertique/vertique-json-schema-validation/0.3.0)

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.

---

## When To Use It

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.

---

## Core Concepts

### Two bound dimensions

`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.

### Value-free by construction

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.

### Pattern walk at registration time

`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.

### Violation detail shape

`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.

### `rfc6901` conversion

`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.

### No configuration

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.

---

## Key Classes

### PatternInputGuard

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.

```java
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();
}
```

### SchemaKeywords

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).

### JsonSchemaValidationOptions

`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 and PatternCompileFailure

`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.

### PathForm

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

### ViolationDetails

- `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`.

---

## Module Dagger Bindings

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

---

## Dependencies

- `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`.
