feat(compiler): implement @oneOf input objects#1030
Open
Conversation
c3a152f to
26ba17f
Compare
Full implementation of the `@oneOf` RFC per GraphQL spec §3.10.1. Schema: - Add `directive `@oneOf` on INPUT_OBJECT` as a built-in - Add `isOneOf: Boolean!` to `__Type` introspection - Validate: `@oneOf` fields must be nullable, must not have default values - InputObjectType::is_one_of() convenience method Executable documents: - Validate: exactly one non-null field must be provided as a literal value - Validate: a variable used in a `@oneOf` field position must be non-null type - Runtime coerce_variable_values and coerce_argument_values enforce the same "exactly one non-null field" invariant at request time Diagnostics (with graphql-js unstable_compat_message parity): - OneOfInputObjectFieldNonNull - OneOfInputObjectFieldHasDefault - OneOfInputObjectWrongNumberOfFields - OneOfInputObjectNullField - OneOfInputObjectNullableVariable Tests: - 15 validation tests in tests/validation/one_of.rs covering all rules, graphql-js test parity, and actual introspection response assertions - 4 runtime coercion unit tests in resolvers/input_coercion.rs Fuzz: - fuzz/fuzz_targets/one_of.rs — semantic-invariant target covering both schema invariants and the executable coercion path (value.rs coverage lifted from 0% to 67%) - fuzz/corpus/one_of/ seed corpus (7 seeds) for rapid convergence
cargo fuzz coverage writes generated profdata and llvm binaries to fuzz/coverage/ — same category as /corpus and /artifacts, not something to check in. In the next commit, we add a second fuzz target that exercises the `@oneOf` executable-document path directly — coverage output will be generated and inspected there.
The schema-invariant fuzz target (one_of.rs) cannot reach the
document-level `@oneOf` rules in validation/value.rs because a single
text input rarely satisfies both schema and document validity
simultaneously.
This target fixes a rich `@oneOf` schema covering query, mutation,
subscription, list, and nested positions, and fuzzes only the
executable document string — directly exercising:
- validation/value.rs (0% → 70% fuzz coverage, 100% function coverage)
- All three `@oneOf` diagnostic paths confirmed hit by the fuzzer:
OneOfInputObjectWrongNumberOfFields 1,660 hits
OneOfInputObjectNullField 19 hits
OneOfInputObjectNullableVariable 11 hits
- resolvers/input_coercion.rs coercion path
The 30% uncovered regions in value.rs are all pre-existing non-`@oneOf`
value validation paths (Float/Boolean/Enum arguments, RequiredField)
that require a different schema to exercise and are out of scope for
`@oneOf` testing.
Fills in the CHANGELOG entry with the release date (2026-03-25) and bumps the version to 1.32.0 — the `@oneOf` feature warrants a minor bump because it adds public API (InputObjectType::is_one_of, new diagnostics, new __Type.isOneOf introspection field). Also adds five schema-extension tests that were identified as a gap in the original `@oneOf` implementation: - extending_oneof_type_with_nullable_field_is_valid - extending_oneof_type_with_nonnull_field_is_invalid - extending_oneof_type_with_default_value_is_invalid - adding_oneof_via_extension_with_valid_base_type_is_valid - adding_oneof_via_extension_with_nonnull_field_in_base_is_invalid
…neration
apollo-smith
- Add Ty::as_nullable() — strips the outermost NonNull wrapper.
- DocumentBuilder::input_object_type_definition gains a ~1-in-5 chance
of applying `@oneOf`. When it does, every field is forced nullable and
stripped of default values, satisfying all spec invariants before the
directive is inserted.
- Update snapshot for the deterministic seed (three input types now carry
`@oneOf`).
- Bump version to 0.15.3.
fuzz/one_of
- Completely rewritten from a plain-text target to a structure-aware
target using generate_valid_document. The fuzzer now spends its
budget exploring interesting schema shapes rather than discarding
malformed SDL, giving far better coverage of the `@oneOf` validation
logic. Three invariants are asserted on every valid document:
is_one_of() agrees with directive presence, all fields are nullable,
and no field carries a default.
fuzz/one_of_exec
- Add env_logger::try_init() + debug!("{data}") for parity with every
other fuzz target in the suite.
4b0387d to
e15a08d
Compare
clippy::needless_borrow fired on `&schema` at 9 call sites in tests/validation/one_of.rs — the compiler auto-derefs, so the explicit borrow is redundant.
e15a08d to
0085448
Compare
martinbonnin
approved these changes
Apr 1, 2026
martinbonnin
left a comment
There was a problem hiding this comment.
One comment from graphql/graphql-spec#1211 but looks good otherwise!
Nice test suite!
| ); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
This will need to implement graphql/graphql-spec#1211 for schemas like this:
type Query {
foo(arg: A): Int
}
input A @oneOf {
# oh no, we can't create an instance of A
a: A
}
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.
Summary
Implements the
@oneOfinput objects directive, which became part of the stable GraphQL spec in September 2025.@oneOflets a schema author say "exactly one of these fields must be provided" — it's a discriminated-union pattern that the type system enforces rather than leaving to resolver logic.Closes #882.
How it works — three validation layers
The interesting design question is when each constraint should fire. We ended up with three complementary layers:
Schema validation (
validation/input_object.rs) — when a developer writes a@oneOftype, every field must already be nullable and carry no default. These aren't runtime concerns; they're structural invariants the schema has to uphold before any query is written.Static document validation (
validation/value.rs) — when a query is compiled, a variable used to satisfy a@oneOffield must itself be declared non-null. A nullable variable is unsafe because it could legally be omitted at runtime, which would make the field-count invariant impossible to enforce without executing the query. This is the rule that most other implementations miss or leave to runtime. We validated against Apollo Kotlin, Hot Chocolate, Strawberry, graphql-java, and juniper — our static check is stricter than juniper and graphql-java, but strictly correct per spec.Runtime coercion (
request/coerce_variable_values) — when variables arrive at execution time, exactly one field must be provided and it must be non-null. This is the canonical spec rule and the last line of defense for literal values or dynamic inputs the static analysis can't see.What's new in the public API
directive @oneOf on INPUT_OBJECTis now a built-in.InputObjectType::is_one_of() -> bool— clean check without digging into directives.__Type.isOneOf: Boolean!in the introspection schema — returnstruefor@oneOftypes,nullfor everything else (matching the spec's introspection definition).Fuzz coverage
Two complementary fuzz targets:
one_of— structure-aware, usingapollo-smith. We extended smith so it generates@oneOfinput types naturally (with all invariants pre-satisfied at generation time), which means the fuzzer spends its budget on schema shape diversity rather than discarding malformed SDL. Asserts thatis_one_of()always agrees with directive presence, and all field invariants hold.one_of_exec— the schema-invariant target can't reach document-level rules, because valid schema SDL and valid executable SDL are different constraint domains. This second target fixes a rich@oneOfschema and fuzzes only the executable document. It confirmed coverage of all five@oneOf-specific code paths in bothvalidation/value.rsandcoerce_variable_values.Version
apollo-compilerbumps to1.32.0— minor becauseis_one_of()and the introspection field are new public API.apollo-smithbumps to0.15.3— minor becauseTy::as_nullable()is new public API.Notes
The production federation path (Rover + JS composition) preserves
@oneOfthrough the supergraph correctly. Rust in-process composition currently strips all directives from input types during merge — that's a separate pre-existing gap and is tracked as a follow-up, not a blocker for this PR.