Skip to content

checker: report a shared-schema change once, and list where else it applies - #1265

Open
reuvenharrison wants to merge 7 commits into
mainfrom
feat/group-shared-schema-changes
Open

reuvenharrison wants to merge 7 commits into
mainfrom
feat/group-shared-schema-changes

Conversation

@reuvenharrison

@reuvenharrison reuvenharrison commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Implements the #1256 decision, on top of #1263 (the walk memo) and #1264 (the diff carrying component names).

What changes

A schema that several properties of one payload reference is one schema, so a change in it, or below it, is one change to the operation's contract. Each check now reports it once, at one of those properties, and says where else it applies:

the `customerId` response's property pattern `^[0-9]+$` was added for the status `200` (shared schema: Id, also at `userId`)
    A change in a schema that several properties of this payload reach is reported once per check, at one of
    those properties: it is a single change to the contract, and it applies wherever the schema is used.

JSON and YAML carry the full list as a field, so a consumer never has to parse the text:

"sharedSchema": {
  "name": "Id",
  "properties": ["customerId", "userId"]
}

main already drops userId here with no explanation, since #1263 merged; v1.33.0 reported both. This PR keeps the single record and puts the information back.

  • The text names one other property and counts the rest, as in and 1 more, because these paths are long. The field lists every one.
  • One entry per reference, not per path. Each reference to the schema is listed at the path the walk took to it. In a nested spec the number of paths can run to millions; the number of references stays small. The longest list on the GitHub REST API description is 44.
  • A change below the shared schema names it too, and lists the same properties with the rest of the change's path appended.
  • Fingerprints do not change. The fingerprint hashes the arguments, not the text, so the shared schema is context only. A formatter test pins this. docs/FINGERPRINT.md said the text was hashed, and is corrected.
  • The label says "shared schema", because the schema named is the innermost one several references reach, which is not always the one that changed.
  • Deduplication is per check. Every check walks the payload itself, so a shared schema that gained a property and a pattern produces one finding from each check. Each change and each operation also still gets its own line.

A fix to #1264 in the diff

In OpenAPI 3.1 a description beside a $ref overrides the component's, and the parser applies it to a copy of the component. The copy is not the components.schemas entry, so #1264's lookup by schema object missed it and recorded no name. The name now comes from the $ref when that lookup misses. A $ref to a schema inside a component stays unnamed.

The copies keep the component's children, so what several references reach can be an inline child of the component. That is the case on the spec reported in #1252: ten references reach Company's inline users array. Such a schema is named after the component the walk passed through to reach it, so those findings name Company. Without the diff fix they would name MarketPartner, a schema that only references Company. A test fails without either half.

Measured

Adding one optional property to simple-user in the GitHub REST API description (13 MB):

findings explained time
v1.33.0 1015
main 728 none 2.1s
this branch 358 139 2.1s

On the spec from #1252, with one property added inside the cycle: 31 findings on main and here, all 31 now naming Company with ten references each. Fingerprints and finding counts are identical to this branch before the list was added, on both specs.

Known limitation

The walk deduplicates by schema object, and a description beside each $ref gives each reference its own copy. So a property added to Address, where billing and shipping both $ref it with a description, is still reported twice with no explanation. Without the descriptions it is reported once, with the list. That matches v1.33.0, so nothing regresses. Treating the copies as one schema would mean keying the walk on the component name, which could hide a real difference between copies. That needs a separate decision.

Follow-up, not in this PR

Grouping across operations, which is where most of the remaining volume is: of the 358 findings above, 355 are the same property spread over 298 operations. The output has to keep one record per operation, because the fingerprint covers the operation and tools that consume the output key changes by it. So that grouping belongs in presentation: a schema identity field on every change, and a schemas view next to the existing endpoint grouping in the formatters.

Tests

  • userId and customerId share Id: one finding listing both, and three properties render as "and 1 more" (shared_schema_two_properties_*).
  • A schema reached through two parents, each through two properties, is one finding listing the reference through the other parent (shared_schema_two_parents_*).
  • A change below the shared schema lists the same properties with its own path appended.
  • Two checks on the same shared schema each report once.
  • An inline shared schema behind description overrides is named after its component (shared_schema_override_*), and a $ref straight into a component stays unnamed (shared_schema_unnamed_*).
  • The diff names a $ref with an override and leaves a $ref into a component unnamed. Mutation-checked.
  • The published output schema validates a sample that emits sharedSchema.

Full suite, go vet, golangci-lint and gofmt pass.

For the release notes

  • Fingerprints of the dropped duplicates disappear, so a review decision stored against one of them does not carry forward. Surviving findings keep their fingerprints.
  • A new sharedSchema field in JSON and YAML output.
  • An --err-ignore entry written against the old text of a shared-schema finding no longer matches, because the text now carries the schema and the other properties.

🤖 Generated with Claude Code

A schema reached from several properties of one payload is one schema, so
a change in it is one change to the operation's contract. The walk already
reported it once per parent; it now reports it once per schema, so the
same schema reached through two different parents is one finding rather
than two.

The change carries a comment stating the principle, in all four locales,
attached only where more than one property path of the payload reaches
the schema, which the walk counts in a pass that reports nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@codecov-commenter

codecov-commenter commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.06542% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 92.76%. Comparing base (378e7bb) to head (02503bd).

Files with missing lines Patch % Lines
checker/shared_schema.go 96.77% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1265      +/-   ##
==========================================
+ Coverage   92.73%   92.76%   +0.02%     
==========================================
  Files         346      347       +1     
  Lines       14653    14716      +63     
==========================================
+ Hits        13589    13651      +62     
- Misses       1064     1065       +1     
Flag Coverage Δ
unittests 92.76% <99.06%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

A schema several of a payload's property paths reach is walked once, so the
change in it is reported at one of those paths. The path then no longer says
which schema changed, and the other paths that reach it are absent from the
changelog with nothing to account for them.

The change now names the schema, from the component names the diff records on
each node, so the reader has the thing to look at rather than one of its uses:

  added the optional property `first/shared/extra` to the response with the
  `200` status (schema: Shared)

The name is reported for a change below the shared schema too, not only for one
in it: that change is deduplicated the same way, so it owes the same account of
what is missing. A schema that is not a components.schemas entry has no name,
and the comment carries the change on its own.

The name renders after Details rather than into it, so a check that sets its
own details, as the deprecation checks do, does not drop it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@reuvenharrison reuvenharrison changed the title checker: a change in a shared schema is reported once, and says so checker: a change in a shared schema is reported once, and names the schema Oct 2, 2026
reuvenharrison and others added 5 commits October 2, 2026 15:23
`(schema: X)` next to a property path reads as though the property is in X,
and it is not always: the name is the innermost schema several of the
payload's paths reach, which can enclose the one that changed. On the GitHub
REST API description, adding a property to `simple-user` produces five
findings whose path already names Simple User and whose shared schema is
`nullable-integration`, the schema whose second use was dropped.

Also widen the two-parent fixture, which now reaches First and Second through
two properties each and adds a pattern inside Shared, and assert on the
finding for the shared schema rather than on the total, since the payload now
adds properties of its own as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"is reported once" reads as once in the whole changelog. Every check walks the
payload itself, so the schema is walked once per check, and a shared schema
that both gained a property and gained a pattern on an existing one produces
one finding from each check. The comment now says "once per check", in all
four locales.

Two more things the comment does not claim, now stated in the docs and pinned
by a test: two properties added to the same shared schema are two changes,
both at the same property path, and each operation reports the change too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A pattern added to a schema that `userId` and `customerId` both reference
was reported at `customerId` alone. A reviewer looking for `userId` found
nothing, and the two properties can mean different things.

The change now names the other properties, in the text and as data:

  the `customerId` response's property pattern `^[0-9]+$` was added for the
  status `200` (shared schema: Id, also at `userId`)

  "sharedSchema": {"name": "Id", "properties": ["customerId", "userId"]}

The text names one other property and counts the rest, since the paths are
long; JSON and YAML carry every one. The list has one entry per reference to
the schema, each written as the path the walk took to that reference, so it
grows with the number of references rather than with the number of paths
through them, which in a nested spec can run to millions. On the GitHub REST
API description the longest list is 44 and the findings, fingerprints and
running time are unchanged.

The shared schema is context for the reader, not part of the change's
identity: the fingerprint is computed from the arguments, so it does not
change. docs/FINGERPRINT.md said the text was hashed; it is the arguments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In OpenAPI 3.1 a description or summary beside a $ref overrides the
component's, and the parser applies it to a copy of the component. The copy
is not the components.schemas entry, so looking the name up by schema object
missed it, and every such $ref was recorded as having no component name.

On the spec reported in #1252, `company` is `{$ref: Company, description:
...}` in every schema that uses it, so changes reached through it lost the
name entirely, and the checker, which names a schema several properties
share from these fields, had no name to give.

When the object lookup misses, the name now comes from the $ref itself. A
$ref into a component (`#/components/schemas/Holder/properties/inner`) names a
schema inside it and stays unnamed. Every path to a schema yields the same
name, so the result does not depend on which reference the diff met first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A description beside a $ref makes the parser copy the component, and the
copies keep its children. On the spec reported in #1252, `company` is
`{$ref: Company, description: ...}` in each schema that uses it, so what ten
references reach is not Company, which is a different object at each, but
Company's inline `users` array, which they all keep. The array has no name of
its own, so all 31 findings named no schema.

A shared schema written inline is now named after the innermost component the
walk passed through on the way to it, which is the component it belongs to:
those 31 findings name Company. A $ref straight to a schema inside a
component passes through none, and stays unnamed.

This needs the copies to be named, which the diff fix in this branch does.
Without it the innermost named component on that spec is MarketPartner, a
schema that merely references Company. The new test fails without either
half.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@reuvenharrison reuvenharrison changed the title checker: a change in a shared schema is reported once, and names the schema checker: report a shared-schema change once, and list where else it applies Oct 3, 2026

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.

2 participants