Skip to content

Polymorphic input types #238

Description

@oojacoboo

I'd like to go ahead and present this RFC for implementation, possibly early as it looks to have full support. Or, at the least, I'd like to get the discussion and ideas flowing for implementation in the future.

https://github.com/graphql/graphql-spec/blob/master/rfcs/InputUnion.md

This is particularly of interest in cases where you have an update mutation that has optional relational inputs. In this case, you have to support creating and updating for the relation. It's also the case for one:many relationships, if you wish to update relations within a parent mutation.

There are a number of other use cases that are also important. The above use case is one that we're finding to be very common and a continual frustration point with the API/schema.

Activity

  1. moufmouf commented on Feb 8, 2020

    @moufmouf
    Member

    I do understand your frustration.

    GraphQLite uses webonyx/graphql-php under the hood for schema generation and GraphQL parsing.
    I'm all in favor of implementing polymorphic input types but we need first to have it supported by webonyx/graphql-php. So I think you should first open an issue with them.

    Once it is supported in webonyx/graphql-php, it to GraphQLite should be quite easy.

  2. oojacoboo commented on Feb 10, 2020

    @oojacoboo
    CollaboratorAuthor

    The issue has been filed with webonyx now.

    webonyx/graphql-php#619

  3. noknokcody commented on Mar 23, 2024

    @noknokcody

    Been 4 years since this was posted so I'd be keen to see if there's any updates to this ? Been a good point of frustration for me aswell. Currently trying to make some form of workaround using a custom RootTypeMapper.

  4. oojacoboo commented on Mar 23, 2024

    @oojacoboo
    CollaboratorAuthor

    @noknokcody submit a PR with webonyx, then we can get a PR merged here as well.

  5. oojacoboo commented on Jul 8, 2025

    @oojacoboo
    CollaboratorAuthor

    Well, good news - @oneOf has been merged into webonyx now. If someone would like to outline an implementation plan based on this, that'd be a great starting point.

  6. oprypkhantc commented on Aug 5, 2025

    @oprypkhantc
    Contributor

    I'll share some thoughts on designs that, in my opinion, make sense, and ones that don't:

    I wrote an entire chapter on different designs and then realized one important limitations that, fortunately, makes it easier for GraphQLite :) Although all of the fields in a @oneOf input must be declared nullable, none of them can actually accept null. This is counterintuitive and doesn't make any sense to me, but it allows the implementation to be simpler and not require #726 first.

    I'm thinking something like this:

    class UserController {
        #[Query]
        public function find(FindUserBy $by): User 
        {
            $emailProvided = $by->email !== null;
        }
    }
    
    #[Input]
    #[OneOf] 
    // or - #[Input(oneOf: true)]
    // or without #[Input], just the #[OneOf]
    class FindUserBy {
        public function __construct() {
            #[Field] public readonly ?string $email = null,
            #[Field] public readonly ?string $phone = null,
        }
    }

    Which should be pretty straightforward in terms of the implementation, and is somewhat type-safe right away. It's also how some other implementations did it: https://chillicream.com/blog/2022/01/13/hot-chocolate-12-5#oneof-input-objects

    It still got some issues I don't like:

    • technically, both $email and $phone could be filled (for example, when testing the controller directly in a mocked test, not through GQL)
    • it's not possible to "match" based on which input was provided. That is, you have to check each field one by one, until you get to the one you want. In an example above, this is trivial, but if there are 10 fields it starts to get tricky

    An alternative approach solving those issues would be something along the lines of:

    class UserController {
        #[Query]
        public function find(FindUserBy $by): User 
        {
            $emailProvided = $by->email !== null;
        }
    }
    
    #[OneOf]
    interface FindUserBy {}
    
    #[OneOf]
    class FindUserByEmail implements FindUserBy {
        public function __construct(
            #[Field] public readonly string $email,
        ) {}
    }
    
    #[OneOf]
    class FindUserByPhone implements FindUserBy {
        public function __construct(
            #[Field] public readonly string $phone,
        ) {}
    }

    But this is much worse in terms of the implementation, requires a lot of limitations on how the class is built (exactly one implemented interface, exactly one field, a class per each field) and is quite verbose. While I like this more from a type-safety standpoint, I don't think this is the way to go.

    If anyone's got any ideas they're welcomed :) So far I believe only the first design is a viable option, and I can't think of any other alternative to it.

  7. oojacoboo commented on Aug 5, 2025

    @oojacoboo
    CollaboratorAuthor

    After some additional review, I agree @oprypkhantc. There really isn't any other reasonable design.

    As for the attribute, I maintain that keeping the number of attributes more limited in scope is a better design choice. There is also a direct dependency between @oneOf and an input. So, having a different attribute, that could potentially be misused, only leads to confusion.

    #[Input(oneOf: true)] // Let's use an argument
    • technically, both $email and $phone could be filled (for example, when testing the controller directly in a mocked test, not through GQL)

    This is a user-land concern IMO. unless we want to ship a base OneOf class that must be extended, and that's the basis for the implementation. That also could resolve the following issue.

    • it's not possible to "match" based on which input was provided. That is, you have to check each field one by one, until you get to the one you want. In an example above, this is trivial, but if there are 10 fields it starts to get tricky

    Aside from some base class design, we could should ship some helper utilities around this - like a trait (sigh). I agree the DX is sub-optimal if you have to check all these fields. This is also similar to the issues from #726.

    To be clear though, I dislike the idea of using inheritance. This has been avoided in GraphQLite, to date, and I think we should make all efforts to maintain that. It allows for the lib implementation to be much more flexible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions