Test the claim that the exported schema cannot drift - #27
Open
VSN2015 wants to merge 1 commit into
Open
Conversation
VSN2015
commented
Sep 5, 2026
| # | ||
| # Anything the exporter does not emit is out of scope on purpose: this is a | ||
| # measuring instrument, not a general validator. | ||
| module TinyJsonSchema |
Owner
Author
There was a problem hiding this comment.
TinyJsonSchema provides a lightweight, dependency-free validator covering exactly the emitted keywords, keeping the test suite standalone.
| "an exclusive range" => { | ||
| contract: proc { optional :pct, :integer, in: 0...100 }, | ||
| payloads: [[{ "pct" => 0 }, :agree], [{ "pct" => 99 }, :agree], [{ "pct" => 100 }, :agree]] | ||
| }, |
Owner
Author
There was a problem hiding this comment.
Bidirectional assertion checking (expect(documented).to eq(runtime)) effectively proves that client requests valid against the emitted schema will not fail server validation.
"Because the schema is emitted from the same frozen data the server
enforces, the docs cannot drift from the validation" is the gem's
headline claim, and nothing tested it. Every existing spec checks one
side or the other: what the validator accepts, or what the exporter
emits. None checked that the two AGREE.
spec/schema_conformance_spec.rb walks canonical JSON payloads through
both a contract and its own exported schema and compares the verdicts —
scalars and bounds, enums, exclusive and endless ranges, exact lengths,
formats, arrays of scalars and of hashes, nested hashes under
unknown: :error, rooted contracts, and a required string with no
length: of its own.
spec/support/tiny_json_schema.rb is a deliberately small validator
covering exactly the keywords the exporter emits and nothing else: a
measuring instrument, not a dependency. One example asserts the
exporter emits no keyword the validator silently ignores, so the two
cannot fall out of step as the exporter grows.
The spec was mutation-tested rather than trusted for being green.
Dropping maxItems, additionalProperties: false, the required-string
minLength: 1, or the root: required wrapper each makes it fail. The
minLength mutation initially did NOT fail, which was a gap in the case
table rather than the design — that logic only bites on a required
string with no length: of its own, and there was no such case. Adding
one made the mutation fail as it should.
The sweep found no drift in the code, and two places where the schema
and the runtime legitimately differ that were not written down: the
runtime accepts non-canonical encodings ("30" for an :integer, 1 for a
:string), and it reads an explicit null as ABSENCE, which JSON Schema
cannot express. Both are now documented in JsonSchema and the README.
Both leave the schema STRICTER than the server, never looser — a client
validating against the published document is conservative, never
surprised by a 422 — and the spec asserts that direction for every
divergence it permits, so a new one in the dangerous direction fails
CI rather than shipping quietly.
Tests and documentation only; no behaviour changes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
VSN2015
force-pushed
the
test/schema-conformance
branch
from
September 11, 2026 22:01
3039437 to
521759c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The untested claim
That's the gem's headline claim, and nothing tested it. Every existing spec checks one side: what the validator accepts, or what the exporter emits. None checked that the two agree.
The spec
spec/schema_conformance_spec.rbwalks canonical JSON payloads through both a contract and its own exported schema and compares the verdicts — 53 examples across scalars and bounds, enums, exclusive and endless ranges, exact lengths, formats, arrays of scalars and of hashes, nested hashes underunknown: :error, rooted contracts, and a required string with nolength:of its own.spec/support/tiny_json_schema.rbis a deliberately small validator covering exactly the keywords the exporter emits and nothing else — a measuring instrument, not a dependency. One example asserts the exporter emits no keyword the validator silently ignores, so the two can't fall out of step as the exporter grows.It was mutation-tested, not trusted for being green
maxItemsfrom arraysadditionalProperties: falseminLength: 1root:required wrapperThe
minLengthmutation initially did not fail — a gap in my case table, not the design: that logic only bites on a required string with nolength:of its own, and I had no such case. Adding one made the mutation fail as it should. Worth knowing that the table is the coverage, so extending it is how the guarantee grows.What the sweep found
No drift in the code. It did find two places where the schema and the runtime legitimately differ that weren't written down anywhere:
"30"for an:integerand1for a:string, because form and query payloads are all strings. (The exporter's comment already mentioned this one.)nullas absence — the runtime reads{"age": null}as{}; JSON Schema cannot express that, sotype: integerrejects a null the server would accept and ignore. This one was undocumented.Both now appear in
Permittable::JsonSchema's comment and in a new README subsection.The direction is the point
Both divergences leave the schema stricter than the server, never looser — so a client validating against the published document is conservative, never surprised by a 422. The spec asserts that direction for every divergence it permits:
So a new divergence in the dangerous direction — docs promising something the server rejects — fails CI rather than shipping quietly.
Verification
master's field surface; it's the natural place to extend oncenullable:(Add nullable: so a contract can clear a column #13),:json(Add the :json field type for free-form hashes #15) andformat:presets (Add format: presets, which export the JSON Schema format keyword #20) land — each adds schema keywords worth pinning the same way