Three version labels that do different jobs

Our published dataset schema declares the JSON Schema 2020-12 dialect. The dataset carries a date-based version. The tutorial adds a separate schema_version to its example record envelope to choose a consumer contract. The v1/v2 labels below belong to this tutorial.

LabelWhat it identifiesExample here
$schemaThe validation language's dialectJSON Schema 2020-12
Dataset versionThe source data release2026-07-20
Example schema_versionThe record contract a consumer understandsInteger 1 or 2

The JSON Schema dialect documentation defines $schema; it does not define your application's release numbering. Record both the dataset release and the accepted contract versions in a failing test report.

Ground the example in the generator's actual fields

We copied the eight fields of our own published US record into the downloadable example: country, iso2, callingCode, nationalPattern, e164Example, nationalDigits, fixtureSafety and sourceUrl. The phone value is +12025550123; its source note points to NANPA's limited fictional 555 range. This is display/test data, with no SMS operation in the experiment.

We verified the copy against the repository record and validated the complete 31-record dataset against its published schema. We then added tutorial envelope fields schema_version and delivery_allowed: false. That boolean is an explicit policy of this offline example, not a newly inferred field in the public country dataset.

The v1 contract keeps the flat eight-field record. V2 renames e164Example to phone_e164 and groups fixtureSafety and sourceUrl inside provenance. The phone value itself is identical in both versions. A consumer must know where to read it and must retain its provenance.

Our measured producer/consumer matrix

On October 8, 2026 we ran the example with Python 3.13.15 and jsonschema 4.26.0. The legacy consumer accepts only contract 1. The migrating consumer accepts known versions 1 and 2, validates the input, maps v2 into the v1 view and validates that result. These are four explicit combinations, not a compatibility promise for other releases.

ProducerLegacy consumerMigrating consumer
V1 flat fixtureACCEPTACCEPT
V2 nested provenanceUNSUPPORTED_VERSIONACCEPT, mapped to v1 view

The adapted v2 record compared equal to the original v1 record, including the source URL, safety note and false delivery flag. Checking only the phone string would miss a lost provenance field.

Four incompatible records and pinned outcomes

Each case starts from a separate copy. The downloadable script checks its own stable outcome codes rather than depending on a library's full English error message:

Input changeExpected outcomeBoundary to enforce
Remove fixtureSafetyMISSING:fixtureSafetyRequired metadata stays required.
Rename the phone key without changing contract 1MISSING:e164ExampleA new key does not silently replace the old contract.
Use a JSON number for the phone valueTYPE:e164ExampleDo not coerce numbers into phone strings.
Send contract version 99UNSUPPORTED_VERSIONDispatch only explicitly supported versions.

Extra checks rejected a v2 record missing provenance.sourceUrl, a true delivery flag, and a boolean used as the version. The latter matters in Python because a direct equality check can treat True as equal to 1; our dispatcher checks the exact integer type.

These schemas deliberately use a closed object contract. In JSON Schema's object model, properties describes present fields, required requires their presence, and additionalProperties: false closes the object. Closing an object is a compatibility choice: a newly added field can be rejected by an older strict consumer. An open extension area needs its own reviewed policy.

Reproduce the check locally

Download the complete Python experiment and its pinned requirements. The v1 schema and v2 schema are also available for inspection. Install the dependencies, then run the script:

python -m pip install -r phone-fixture-schema-requirements.txt
python phone-fixture-schema-drift.py

The script embeds the reviewed example and schemas, checks their dialect validity, and performs a JSON encode/decode round trip before validation. It makes no network requests or messaging calls. Dependency installation needs access to your package source; the experiment itself runs offline afterward.

Python's JSON module parses and serializes JSON. Parsing a document successfully does not establish a fixture contract. This example applies Draft202012Validator after parsing and checks the schemas with check_schema.

See the core migration boundary
def adapt_v2_to_v1(record):
    check(record, 2)  # Validate the producer record first.
    row = {key: deepcopy(record[key]) for key in v2_strings}
    row.update(
        schema_version=1,
        e164Example=record["phone_e164"],
        fixtureSafety=record["provenance"]["fixtureSafety"],
        sourceUrl=record["provenance"]["sourceUrl"],
        delivery_allowed=record["delivery_allowed"],
    )
    check(row, 1)  # Validate the consumer view too.
    return row

Upgrade the consumer before the producer

  1. Write down the field delta. Identify renames, moved fields, type changes and delivery/provenance requirements.
  2. Store old and new golden records. Keep their expected outcomes and dataset release with the consumer tests.
  3. Support both known contracts. Deploy the migrating consumer while the producer still emits v1.
  4. Validate before mapping. A missing source or safety note is an error, not a reason to fill in a default.
  5. Switch the producer deliberately. Re-run the matrix against every participating consumer before emitting v2 fixtures.
  6. Retire v1 with evidence. Remove its adapter only after the consumer inventory confirms it is no longer required.

Failing early at this boundary makes a schema mismatch visible before the main CI scenarios use the fixture. Keep phone parsing, fictional-range review and live delivery authorization as separate checks; schema validation establishes a record shape, not a working line.

FAQ from the compatibility boundary

A public developer question describes misspelled nested property names being accepted. It is one question signal, not evidence of how often this happens. Our field-name and metadata cases turn that concern into explicit fixture checks.

Can a fixture keep the same digits but break a consumer?

Yes. In our v2 example the phone value is unchanged, but its key is renamed and the provenance moves. A v1-only consumer rejects the unknown contract version.

Should unknown schema versions be accepted?

This example rejects them. Accepting a version without a reviewed schema or adapter would leave the field and safety policy undefined.

How do I migrate without dropping safety metadata?

Require the source and safety note in the incoming schema, map them explicitly, then validate the consumer view and compare the complete reviewed record.

Why can a schema accept a misspelled field?

Declaring properties alone does not require those keys or reject extra keys. Review required and the additional-properties policy at each relevant object level.

Does JSON parsing validate a generated fixture?

No. Parsing returns JSON values; apply the intended contract validator and version dispatch before consuming them.

Keep the next check focused

Use the phone format dataset to inspect field names and source notes. The test-oracle experiment answers whether an assertion notices defects; this page answers which record versions a consumer can accept. Both stay offline.

Original material: eight measured producer/consumer and invalid-record cases, three extra rejection checks, a provenance-preserving adapter, complete schemas, runnable code and the original boundary diagram. Results describe this example only.