Skip to content

feat: Add Support for @oneOf Input Object Directive #1715

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jul 8, 2025

Conversation

jasonbahl
Copy link
Contributor

Add Support for @oneOf Input Object Directive

Summary

This PR implements the @oneOf input object directive to match the GraphQL.js reference implementation (v16.11.0) and align with the GraphQL specification RFC #825.

The @oneOf directive enables input objects where exactly one field must be provided, addressing the common pattern of needing "input unions" in GraphQL schemas.

Implementation Details

Core Type System Changes

  • InputObjectType: Added isOneOf property with proper type annotations, isOneOf() method, and validateOneOfConstraints() for spec compliance validation
  • Directive: Added @oneOf directive definition with INPUT_OBJECT location
  • Introspection: Added isOneOf field to __Type introspection object

Validation & Coercion

  • OneOfInputObjectsRule: New validation rule ensuring exactly one field is provided in OneOf input objects
  • Enhanced coercion validation: Added OneOf validation to Value::coerceInputValue() for runtime coercion errors
  • Null value validation: Prevents explicitly null field values in OneOf inputs per spec requirements

GraphQL.js Compatibility & Spec Alignment

Matching GraphQL.js v16.11.0

This implementation aligns with the GraphQL.js reference implementation, following the same patterns for:

  • Type system integration (isOneOf property)
  • Introspection support (isOneOf field)
  • Query validation (OneOf validation rule)
  • Schema validation (OneOf constraint validation)

Enhanced Spec Compliance

Based on GraphQL.js PR #4195 and spec RFC #825, we've implemented additional validation:

  • Runtime coercion validation: OneOf validation during input value coercion phase (matching PR #4195)
  • Null field validation: Explicit null value detection for OneOf fields
  • Comprehensive error messaging: Clear error messages for all OneOf validation scenarios

Test Coverage

This implementation includes comprehensive test coverage in tests/Type/OneOfInputObjectTest.php:

Basic OneOf Type Definition

  • OneOf input object creation and configuration
  • Schema-level validation of OneOf constraints
  • Type introspection verification

Query Validation

  • Valid OneOf input validation (exactly one field)
  • Invalid input rejection (zero fields, multiple fields)
  • Null value validation and error handling

Runtime Coercion Validation

  • OneOf validation during Value::coerceInputValue()
  • Proper error handling for invalid OneOf inputs
  • Null field value detection and rejection

Edge Cases and Error Scenarios

  • Complex OneOf validation scenarios
  • Integration with existing GraphQL validation
  • Backward compatibility verification

Corresponding GraphQL.js Tests

For reference, test coverage matches the GraphQL.js implementation found in:

Breaking Changes

None. This is a purely additive feature that maintains full backward compatibility.

Migration Guide

No migration required. Existing schemas continue to work unchanged. To use OneOf:

$oneOfInput = new InputObjectType([
    'name' => 'SearchInput', 
    'fields' => [
        'byId' => Type::id(),
        'byName' => Type::string(),
        'byEmail' => Type::string(),
    ],
    'isOneOf' => true,
]);

Validation Results

  • ✅ All 1,872 tests passing
  • ✅ 21,070 assertions successful
  • ✅ PHPStan analysis clean
  • ✅ Spec-compliant implementation
  • ✅ Runtime coercion validation included

Copy link
Collaborator

@spawnia spawnia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work! Happy to include this once polished.

jasonbahl and others added 3 commits July 4, 2025 09:05
Co-authored-by: Benedikt Franke <[email protected]>
…e fields

- removed test and corresponding InvariantViolation that oneOf input types must not have deprecated fields. This was a mistake. I think in my mind I was thinking that if only 1 field was defined it should not be deprecated, but this doesn't match that intent and that's not part of the spec from what I can tell anyway so I've removed it
@jasonbahl jasonbahl requested a review from spawnia July 4, 2025 16:34
@jasonbahl
Copy link
Contributor Author

@spawnia thanks so much for the fast and thoughtful review! I pushed up some changes based on your feedback. Let me know if you see anything else that needs addressed. Thanks! 🙌

Copy link
Collaborator

@spawnia spawnia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nicely done! Thanks for working with me to add the final polish for this feature.

@spawnia spawnia merged commit f41ed56 into webonyx:master Jul 8, 2025
18 checks passed
spawnia added a commit that referenced this pull request Jul 8, 2025
@spawnia
Copy link
Collaborator

spawnia commented Jul 8, 2025

Released with https://github.com/webonyx/graphql-php/releases/tag/v15.21.0. Thank you @jasonbahl for your excellent work on this.

@ruudk
Copy link
Contributor

ruudk commented Jul 8, 2025

@jasonbahl thanks for this amazing Pr. Could it be that SchemaPrinter::doPrint does not support dumping the @oneOf directive yet?

@ruudk
Copy link
Contributor

ruudk commented Jul 8, 2025

Because if I understand the RFC correctly, it should add the @oneOf on the input type.

input PetInput @oneOf {
  cat: CatInput
  dog: DogInput
  fish: FishInput
}

Currently there is only a test that guards that the directive is declared on the schema. But no test for the input object type.

@jasonbahl
Copy link
Contributor Author

@ruudk thanks for calling this out. I do see a test in graphql-js for this (https://github.com/graphql/graphql-js/blob/8dc295551f9a64276442ef850943794db6bcaa4a/src/utilities/__tests__/printSchema-test.ts#L506-L521). I will follow up with a test for this case.

@jasonbahl
Copy link
Contributor Author

@ruudk @spawnia I opened #1727 to address the missing @oneOf support when printing the schema.

Thanks again for calling this out @ruudk! Hopefully this is all I missed! 👀 🤞🏻

@ruudk
Copy link
Contributor

ruudk commented Jul 9, 2025

@jasonbahl Another issue I noticed, is that on the Introspection query result, the isOneOf boolean is missing on the INPUT_OBJECT object. See https://github.com/graphql/graphql-spec/pull/825/files#diff-5c333e0b20dda16f4725f894df405fde1f42f5fac752a0e3b9eb0680a64a2681R378

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.

3 participants