Skip to content

[RFC] GraphQL Input Union type #488

Closed
@frikille

Description

@frikille

[RFC] GraphQL Input Union type

Background

There have been many threads on different issues and pull requests regarding this feature. The main discussion started with the graphql/graphql-js#207 on 17/10/2015.

Currently, GraphQL input types can only have one single definition, and the more adoption GraphQL gained in the past years it has become clear that this is a serious limitation in some use cases. Although there have been multiple proposals in the past, this is still an unresolved issue.

With this RFC document, I would like to collect and summarise the previous discussions at a common place and proposals and propose a new alternative solution.

To have a better understanding of the required changes there is a reference implementation of this proposal, but that I will keep up to date based on future feeback on this proposal.

The following list shows what proposals have been put forward so far including a short summary of pros and cons added by @treybrisbane in this comment

  • __inputname field by @tgriesser

    RFC document: RFC: inputUnion type #395

    Reference implementation: RFC: inputUnion type graphql-js#1196

    This proposal was the first official RFC which has been discussed at the last GraphQL Working Group meeting.
    This proposal in this current form has been rejected by the WG because of the __inputname semantics. However, everyone agrees that alternative proposals should be explored.

    • Pros:
      • Expresses the design intent within the schema
      • Supports unions of types with overlapping fields
      • Removes the costs of the tagged union pattern (both to the schema and field resolution)
      • Addresses the feature asymmetry of unions within the type system
    • Cons:
      • Adds complexity to the language in the form of input union-specific syntax
      • Adds complexity to the language in the form of additional validation (around __inputtype, etc)
      • Adds complexity to the request protocol in the form of a (input union-specific) constraint
  • Tagged union by @leebyron and @IvanGoncharov

    Original comment

    input MediaBlock = { post: PostInput! } | { image: ImageInput! }
    • Pros:
      • Expresses the design intent within the schema
      • Removes the costs of the tagged union pattern (both to the schema and field resolution)
      • Does not require changes to the request protocol
    • Cons:
      • No support for unions of types with overlapping fields
      • Introduces inconsistencies between the syntax for output unions and the syntax for input unions
      • Adds complexity to the language in the form of input union-specific syntax
      • Adds complexity to the language in the form of additional validation (around enforcing the stipulations on overlapping fields, nullability, etc)
      • Does not fully address the feature asymmetry of unions within the type system
  • Directive

    Original comment

    input UpdateAccountInput @inputUnion {
      UpdateAccountBasicInput: UpdateAccountBasicInput
      UpdateAccountContactInput: UpdateAccountContactInput
      UpdateAccountFromAdminInput: UpdateAccountFromAdminInput
      UpdateAccountPreferencesInput: UpdateAccountPreferencesInput
    }
    • Pros:
      • Requires no language or request prototcol changes beyond a new directive
      • Supports unions of types with overlapping fields
    • Cons:
      • Adds complexity to the language in the form of a new directive
      • Does not express the design intent within the schema (the presence of a directive completely changes the meaning of a type definition which would otherwise be used to describe a simple object type)
      • Does not remove the costs of the tagged union pattern
      • Does not address the feature asymmetry of unions within the type system

Proposed solution

Based on the previous discussions I would like to propose an alternative solution by using a disjoint (or discriminated) union type.

Defining a disjoint union has two requirements:

  1. Have a common literal type field - called the discriminant
  2. A type alias that takes the union of those types - the input union

With that our GraphQL schema definition would be the following (Full schema definition)

literal ImageInputKind
literal PostInputKind

input AddPostInput {
  kind: PostInputKind!
  title: String!
  body: String!
}

input AddImageInput  {
  kind: ImageInputKind!
  photo: String!
  caption: String
}

inputUnion AddMediaBlock = AddPostInput | AddImageInput

input EditPostInput {
  inputTypeName: PostInputKind!
  title: String
  body: String
}

input EditImageInput {
  inputTypeName: ImageInputKind!
  photo: String
  caption: String
}

inputUnion EditMediaBlock = EditPostInput | EditImageInput

type Mutation {
  createMedia(content: [AddMediaBlock]!): Media   
  editMedia(content: [EditMediaBlock]!): Media
}

And a mutation query would be the following:

mutation {
  createMedia(content: [{
    kind: PostInputKind
    title: "My Post"
    body: "Lorem ipsum ..."
  }, {
    kind: ImageInputKind
    photo: "http://www.tanews.org.tw/sites/default/files/imce/admin/user2027/6491213611_c4fc290a33_z.jpg"
    caption: "Dog"
  }]) {
    content {
      ... on PostBlock {
        title
        body
      }
      ... on ImageBlock {
        photo
        caption
      }
    }
  }
}

or with variables

mutation CreateMediaMutation($content: [AddMediaBlock]!) {
  createMedia(content: $content) {
    id
    content {
      ... on PostBlock {
        title
        body
      }
      ... on ImageBlock {
        photo
        caption
      }
    }
  }
}
{
  "content": [{
    "kind": "PostInputKind",
    "title": "My Post",
    "body": "Lorem ipsum ...",
  }, {
    "kind": "ImageInputKind",
    "photo": "http://www.tanews.org.tw/sites/default/files/imce/admin/user2027/6491213611_c4fc290a33_z.jpg",
    "caption": "Dog"
  }]
}

New types

Literal type

The GraphQLLiteralType is an exact value type. This new type enables the definition a discriminant field for input types that are part of an input union type.

Input union type

The GraphQLInputUnionType represent an object that could be one of a list of GraphQL Input Object types.

Input Coercion

The input union type needs a way of determining which input object a given input value correspond to. Based on the discriminant field it is possible to have an internal function that can resolve the correct GraphQL Input Object type. Once it has been done, the input coercion of the input union is the same as the input coercion of the input object.

Type Validation

Input union types have a potential to be invalid if incorrectly defined:

  • An input union type must include one or more unique input objects
  • Every included input object type must have:
    • One common field
    • This common field must be a unique Literal type for every Input Object type

Using String or Enum types instead of Literal types

While I think it would be better to add support for a Literal type, I can see that this type would only be useful for Input unions; therefore, it might be unnecessary. However, it would be possible to use String or Enum types for the discriminant field, but in this case, a resolveType function must be implemented by the user. This would also remove one of the type validations required by the input type (2.2 - The common field must be a unique Literal type for every Input Object type).

Final thoughts

I believe that the above proposal addresses the different issues that have been raised against earlier proposals. It introduces additional syntax and complexity but uses a concept (disjoint union) that is widely used in programming languages and type systems. And as a consequence I think that the pros of this proposal outweigh the cons.

  • Pros

    • Expresses the design intent within the schema
    • The discriminant field name is configurable
    • Supports unions of types with overlapping fields
    • Addresses the feature asymmetry of unions within the type system
    • Does not require changes to the request protocol
  • Cons

    • Adds complexity to the language in the form of literal-specific syntax (might not need)
    • Adds complexity to the language in the form of input union-specific syntax
    • Adds complexity to the language in the form of additional validations (input union, discriminant field, input union resolving (might not need))

However, I think there are many questions that needs some more discussions, until a final proposal can be agreed on - especially around the new literal type, and if it is needed at all.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions