Skip to content

fix(findSchemaChanges): detect @oneOf added to or removed from an input object type - #4847

Open
eastagiletracker wants to merge 1 commit into
graphql:17.x.xfrom
eastagiletracker:agile-board/detect-oneof-input-object-changes
Open

fix(findSchemaChanges): detect @oneOf added to or removed from an input object type#4847
eastagiletracker wants to merge 1 commit into
graphql:17.x.xfrom
eastagiletracker:agile-board/detect-oneof-input-object-changes

Conversation

@eastagiletracker

Copy link
Copy Markdown

This PR proposes teaching findSchemaChanges to detect @oneOf being added to or removed from an input object type, which is currently reported as no change at all. We include this PR work along with a full history of your repo at https://eastagiletracker.com/projects/349. You can sign in with your GitHub ID to claim ownership of the project.

What goes wrong

Adding @oneOf to an existing input object type is a breaking change: afterwards exactly one field may be supplied and its value must be non-null, so any operation that passed two fields of that input object stops validating.

findInputObjectTypeChanges in src/utilities/findSchemaChanges.ts compares each input field's type, default value and description, but never compares isOneOf on the type itself the way findDirectiveChanges compares isRepeatable. As a result findSchemaChanges, findBreakingChanges and findDangerousChanges all return an empty array for that edit, and a schema check built on them waves through a change that breaks every client sending more than one field.

isOneOf is observable to clients — it is part of the introspection query, printSchema emits @oneOf, and buildClientSchema round-trips it — so it is in scope for a diff whose stated purpose is comparing "client's observable changes".

Reproduction on 17.x.x at 9c24501

import {
  buildSchema,
  findBreakingChanges,
  findSchemaChanges,
  parse,
  validate,
} from './src/index.ts';

const oldSchema = buildSchema(`
  input SearchBy { name: String, id: ID }
  type Query { search(by: SearchBy!): String }
`);
const newSchema = buildSchema(`
  input SearchBy @oneOf { name: String, id: ID }
  type Query { search(by: SearchBy!): String }
`);

const document = parse('{ search(by: { name: "a", id: "1" }) }');
console.log(findSchemaChanges(oldSchema, newSchema));
console.log(findBreakingChanges(oldSchema, newSchema));
console.log(validate(oldSchema, document).map((e) => e.message));
console.log(validate(newSchema, document).map((e) => e.message));
$ node --experimental-strip-types repro.ts
[]
[]
[]
[
  'Within OneOf Input Object type "SearchBy", exactly one field must be specified, and the value for that field must be non-null.'
]

The operation validates cleanly against the old schema and is rejected by the new one, and the reported difference between those two schemas is empty. With this change the first two lines become the INPUT_OBJECT_ONE_OF_ADDED entry below.

The change

findInputObjectTypeChanges now compares isOneOf, mirroring the isRepeatable comparison in findDirectiveChanges, and reports one of two new members:

  • BreakingChangeType.INPUT_OBJECT_ONE_OF_ADDED@oneOf was added to SearchBy. Requiring exactly one non-null field rejects input values that were previously accepted.
  • SafeChangeType.INPUT_OBJECT_ONE_OF_REMOVED@oneOf was removed from SearchBy. Every value valid under a OneOf Input Object stays valid once the constraint is lifted, and such a type's fields are already all nullable and default-free, so nothing becomes required in the other direction.

The names follow the existing DIRECTIVE_REPEATABLE_ADDED / DIRECTIVE_REPEATABLE_REMOVED pair. Both members are additive: no existing member, description string or classification moves, so consumers are unaffected unless they switch exhaustively over the change types. This is the same shape of gap as the input-field default value one closed in #4832, in the same function.

website/pages/api-v* are generated snapshots, so the two members are left for the next docs regeneration; the schema-evolution guide reads them through Object.values(BreakingChangeType) and needs no edit.

Verification

Four tests: @oneOf added, @oneOf removed, and an unchanged OneOf Input Object that gains a field (which must still report only OPTIONAL_INPUT_FIELD_ADDED) in findSchemaChanges-test.ts, plus one through the public findBreakingChanges wrapper in findBreakingChanges-test.ts.

Reverting only the new comparison while keeping the two members turns three of them red, and the unchanged-@oneOf control stays green:

$ npm run node:test -- "src/utilities/__tests__/findSchemaChanges-test.ts" "src/utilities/__tests__/findBreakingChanges-test.ts"
  ✖ should detect if an input type became a oneOf input type
  ✖ should detect if an input object becomes a oneOf input object
  ✖ should detect if an input object stops being a oneOf input object
  ✔ should not detect a change when an input object stays a oneOf input object
ℹ pass 56
ℹ fail 3

Restoring it, all four pass and the rest of the suite is unmoved from the same checks run on a clean 9c24501 tree: npm run testonly goes from 3031 passing / 0 failing to 3035 passing / 0 failing, and npm run testonly:cover keeps findSchemaChanges.ts and the project total at 100.00 lines / 100.00 branches / 100.00 functions. npm run check:ts, npm run lint, npm run prettier:check and npm run check:spelling are all clean.

How this was managed

We tracked this work on a board imported from this repository's issues — 1,170 stories and 15 labels. The story behind this change is https://eastagiletracker.com/projects/349/stories/218725 and the board it sits on is https://eastagiletracker.com/projects/349.

board

If you'd rather not receive contributions like this, reply no-more-prs on this pull request and we won't open any further ones on your repositories.


Lawrence W. Sinclair
CEO / East Agile
linkedin.com/in/lwsinclair/
eastagile.com

@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown

@eastagiletracker is attempting to deploy a commit to the The GraphQL Foundation Team on Vercel.

A member of the Team first needs to authorize it.

@linux-foundation-easycla

Copy link
Copy Markdown

CLA Not Signed

@yaacovCR

Copy link
Copy Markdown
Contributor

a ping on the CLA

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