Skip to content

Use one pattern violation wording in every target - #218

Draft
chrsmith wants to merge 2 commits into
mainfrom
chrsmith/pattern-violation-quotes
Draft

chrsmith wants to merge 2 commits into
mainfrom
chrsmith/pattern-violation-quotes

Conversation

@chrsmith

@chrsmith chrsmith commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

Summary

When a string fails a JSON Schema pattern (or a propertyNames key fails one), the four targets produced four different messages. Java and Python even quoted the pattern after the generator rewrote it for their regex engines ($ → \z / \Z), so users saw a pattern they never wrote. This PR defines one exact wording in specs/json-schema/features/pattern.md and makes Go, Java, Python, and TypeScript produce byte-identical messages for the same input.

Matching behavior is unchanged: only the human-readable reason text of violations changes. Violation paths, which values are accepted or rejected, and the PayloadValidationError shape are unaffected.

Two commits, reviewable separately:

  1. Quote the authored pattern in Java and Python — small, targeted fix for the \z / \Z leak.
  2. Use the spec's wording in every target — defines the exact form and aligns all four targets; most of the diff.

User-visible changes

The new wording

Check Message
pattern on a value must match pattern "^[a-z]+$", got "AB1"
pattern on a propertyNames key invalid property name "Bad": must match pattern "^[a-z]+$"

Every quoted part (pattern, value, key) is a JSON string literal: " and \ are backslash-escaped, \b \f \n \r \t use short escapes, other control characters become \u00xx, everything else is kept as-is. The quoted pattern is the authored one.

Value pattern failures — before → after

Example: pattern ^[a-z]+$, value AB1.

Target Before After
Go must match pattern "^[a-z]+$", got "AB1" unchanged, except control characters now use JSON escapes (\u0001 instead of Go's \x01)
Java must match pattern ^[a-z]+\z, got AB1 must match pattern "^[a-z]+$", got "AB1"
Python must match pattern ^[a-z]+\Z, got "AB1" must match pattern "^[a-z]+$", got "AB1"
TypeScript must match pattern ^[a-z]+$, got "AB1" must match pattern "^[a-z]+$", got "AB1"

propertyNames pattern failures — before → after

Example: pattern ^[a-z]+$, key Bad.

Target Before After
Go invalid property name "Bad": must match pattern ^[a-z]+$ invalid property name "Bad": must match pattern "^[a-z]+$"
Java must match pattern ^[a-z]+\z, got Bad invalid property name "Bad": must match pattern "^[a-z]+$"
Python invalid property name "Bad": must match pattern ^[a-z]+\Z invalid property name "Bad": must match pattern "^[a-z]+$"
TypeScript invalid property name "Bad": must match pattern ^[a-z]+$, got "Bad" invalid property name "Bad": must match pattern "^[a-z]+$"

Other propertyNames failures (length, enum, format)

The invalid property name <key>: prefix now JSON-quotes the key in every target. For ordinary keys nothing changes; for a key containing ", \, or control characters:

  • Java and TypeScript previously wrapped the raw key in quotes without escaping (invalid property name "a"b": …); now invalid property name "a\"b": ….
  • Go previously used %q (Go escapes); now JSON escapes.
  • Python already used JSON quoting — unchanged.

Additional, minor bug fixes

  • Java printed offending values unquoted (got AB1), so an empty string or one with trailing spaces was invisible. Values are now quoted like every other target.
  • Java's propertyNames pattern failure had no invalid property name prefix, unlike every other propertyNames failure.
  • TypeScript appended a redundant , got "<key>" to propertyNames pattern failures; the prefix already names the key.
  • Go could garble propertyNames pattern messages: the pattern was spliced into a fmt.Sprintf format string, so a % in a pattern was misread as a verb. Messages are now concatenated.
  • pattern.md documented the TypeScript message incorrectly (it showed ${PAT_RE}; the code embeds the pattern text).

Implementation notes

  • The quoted pattern is computed once, in Rust (src/json_schema/pattern.rs violation_reason), and every generator embeds the same bytes; host-engine rewrites are still what's compiled for matching.
  • Runtime quoting of values and keys uses each target's JSON quoting: TypeScript JSON.stringify, Python's existing _quote, plus two small new runtime helpers — Go quoteValue and Java Violation.quote(String) — which appear in every regenerated Go/Java runtime file.
  • PRINCIPLES.md P11 (reason text is informative, not compared across targets) gains an exception: a feature spec may define an exact reason form, as pattern.md now does.

Reviewing the diff

44 files, but most are mechanical:

  • Generators (the real change): src/generator/json_schema/{go,java,python,typescript}.rs, src/json_schema/pattern.rs.
  • Specs: features/pattern.md, features/propertyNames.md, PRINCIPLES.md; CHANGELOG.md (Unreleased → Fixed).
  • Tests: new tests/json_schema_violation_reasons.rs generates one schema into all four targets, runs it, and asserts identical (path, reason) lists — including a pattern containing " and \, a control-character value, and a quoted key. Plus updated assertions in tests/generate_{java,python}.rs and each target's sample tests.
  • Regenerated samples (samples/, advanced/samples/): only reason strings and the new Go/Java quoting helpers.

Not in this PR

propertyNames format failures are still inconsistent (Java lacks the prefix; TypeScript appends , got). Same class of fix; left out to keep this PR to pattern.

Validation

cargo validate and cargo test --all-features pass.

🤖 Generated with Claude Code

chrsmith and others added 2 commits October 7, 2026 13:23
Java and Python compiled the per-target `$` end-anchor rewrite (`\z` /
`\Z`) and also quoted that rewritten text in the `must match pattern`
reason, so their messages diverged from Go and TypeScript. Keep the
rewrite for matching only and quote the authored (loader-normalized)
pattern in the reason, including `propertyNames` pattern violations.
Python now emits the authored text as a plain string literal instead of
reading `.pattern` off the compiled regex.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pattern.md's example reason is `must match pattern "^[a-z]+$", got "AB1"`,
but only Go quoted the pattern (with `%q`); Java, Python and TypeScript
printed it bare, Java also printed the offending value bare, and the
`propertyNames` form differed per target: Java dropped the
`invalid property name "k": ` prefix and TypeScript appended `, got …`.

Define the exact form in pattern.md and propertyNames.md and emit it
everywhere. The generator quotes the authored pattern once as a JSON string
(`json_schema::pattern::violation_reason`) and each backend embeds those
bytes; offending values and property-name keys are JSON-quoted at runtime
by `JSON.stringify`, Python `_quote`, and new Go `quoteValue` / Java
`Violation.quote` helpers, so text containing `"`, `\` or control
characters renders identically. A `propertyNames` pattern failure reads
`invalid property name "Bad": must match pattern "^[a-z]+$"`. Go's
`propertyNames` pattern also no longer splices the pattern into a
`fmt.Sprintf` format string, where a `%` would have been misread.

tests/json_schema_violation_reasons.rs drives the same wire values through
all four runtimes and asserts byte-identical reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant