graphql / graphql-spec

GraphQL is a query language and execution engine tied to any backend service.
https://spec.graphql.org
14.28k stars 1.12k forks source link

[RFC] GraphQL Input Union type #488

Closed frikille closed 4 years ago

frikille commented 6 years ago

[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

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:

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.

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.

IvanGoncharov commented 6 years ago

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.

This is the main point of this discussion, instead of proposing competing solutions we should focus on defining a set of problems that we want to address. From previous discussions, it looks like the main use case is "super" mutations. If this is also the case for you RFC should address why you can't do separate createMediaPost or createMediaImage. Do you have any other use real-life use cases that you want to address with this feature?

So this discussion should be driven by real-life user cases and the proposed solution should be presented only in the context of solving this use cases. IMHO, without a defined set of the use cases, this discussion looks like a beauty contest for SDL syntax and JSON input.

Does not address the feature asymmetry of unions within the type system

Feature asymmetry between input and output types is not an issue since they are fundamentally different in API case.

Output types represent entities from the business domain (e.g. Post, Image, etc.) and because of that, they are usually reused all around the schema.

Input types represent set of data required for a particular action or query (e.g. AddPostInput, EditPostInput) and thus in the majority of cases, input types have very limited reusability.

IMHO, that's why it makes sense to sacrifice some simplicity to allow better expressing your domain entities and facilitate reusability by allowing interfaces and unions on output types.

As for input types instead of the feature-reach type system, we need to provide set of validation constraints, e.g. based on your example this type will satisfy:

type AddMediaBlock {
  kind: PostInputKind!
  title: String
  body: String
  photo: String
  caption: String
}

It's working and fully solve the present problem there are the only two problems:

So instead of creating two additional types and complicating type system with inputUnion we can just solve the above problems by describing required validation constraints: type AddMediaBlock = { ... } | { ... }.

Using String or Enum types instead of Literal types

IMHO, literal is just polute namespace and force to use PostInputKind istead of Post. We shouldn't mix client data with typenames from typesystem in any shape or form especially since it was one of main arguments against __inputname.

So it should be either in place defined strings or enum values.

frikille commented 6 years ago

From previous discussions, it looks like the main use case is "super" mutations. Do you have any other use real-life use cases that you want to address with this feature?

Yes. We are building a headless content management platform: users can define their own content types, fields and valiations. They get a GraphQL API that they can use to read and write data. And we want to support a new feature whereby a content type field can have multiple types. So an output type would be something like this:

type PageDocument {
  id: ID
  title: String
  slug: String
  author: Author
  sections: [Section]
}

type Section {
  title: String
  blocks: [SectionBlock]
}

union SectionBlock = ImageGallery | HighlightedImage | ArticleList | RichText

As you can see, this doesn't requires a top level "super" mutation. I agree with you on that a super mutation can and should be just separate mutations. But in our case it is not not possible since the input field that can have multiple types is deeply nested.

An example schema for the input types:

mutation createPageDocument(document: PageDocumentInput!): PageDocument

input PageDocumentInput {
  title: String!
  slug: String!
  author: ID!
  sections: [SectionInput]
}

input SectionInput = {
  title: String!
  blocks: [SectionBlockInput]
}

enum SectionBlockInputEnum = {
  ImageGallery
  HighlightedImage
  Articles
  RichText
}

input ImageGalleryInput = {
  kind: SectionBlockInputTypeEnum!
  title: String!
  description: String!
  imageUrls: [String!]!
}

input HighlightedImageInput {
  kind: SectionBlockInputTypeEnum!
  url: String!
  caption: String!
}

input ArticaleListInput {
  kind: SectionBlockInputTypeEnum!
  articleIds: [ID!]!
}

input RichTextInput {
  kind: SectionBlockInputTypeEnum!
  html: String
  markdown: String
}

I deliberately left out the definition of the SectionBlockInput input type. Based on my proposal it would be this:

inputUnion SectionBlockInput = ImageGalleryInput | HighlightedImageInput | ArticleListInput | RichTextInput

I would skip the "super" SectionBlockInput type definition, that includes every field from the four input types, because as you mentioned that have two problems:

With your required validation constraint suggestion I suppose it would be something like this:

input SectionBlockInput = {  
  kind: SectionBlockInputTypeEnum!
  title: String!
  description: String!
  imageUrls: [String!]!
} | {
  kind: SectionBlockInputTypeEnum!
  url: String!
  caption: String!
} | {
  kind: SectionBlockInputTypeEnum!
  articleIds: [ID!]!
} | {
  kind: SectionBlockInputTypeEnum!
  html: String
  markdown: String
}

I think these two examples are pretty close to each other and would achieve the same result. The main question is that it would be better doing this:

IMHO, literal is just polute namespace and force to use PostInputKind istead of Post. We shouldn't mix client data with typenames from typesystem in any shape or form especially since it was one of main arguments against __inputname.

I accept this.It had a lot of advantages using it when I implemented the input type resolver. I would be happy to drop this one completely as it already felt a bit unnecessary.

IvanGoncharov commented 6 years ago

@frikille Thank you for sharing your use case it helped me a lot with understanding your particular problem. Is it the only type of scenarios where you need Input Union types?

frikille commented 6 years ago

There are some other places where it would be useful, but those are top level mutations (creating a document) which can be solved other ways. However the input union support would help with that use case as well. Our main problem is that we have completely different schemas for every user project, and it would be better if we were able to use a common mutation for creating a document instead of dynamically changing the mutation name. But I understand that it can be solved without input unions, and we've been using that solution for a long time so for that it is not necessary. However, we don't see how the use case I described in my previous comment can be solved without input unions.

jturkel commented 6 years ago

Creating/updating an entity (PageDocument in the example above) with a heterogeneous collection of nested value objects (different types of Section in the example above) is a great use case for input unions. If you're looking for more use cases @xuorig describes batching multiple mutating operations (each of which may have very different structure) into a single atomic mutation in this blog post. The post describes a project board like Trello where a user can add/move/delete/update cards in a single atomic operation. The corresponding SDL looks something like this with input unions:

type Mutation {
  updateBoard(input: UpdateBoardInput!): UpdateBoardPayload!
}

type UpdateBoardInput {
  operations: [Operation!]!
}

union Operation = AddOperation | UpdateOperation |  MoveOperation | DeleteOperation

input AddOperation {
  id: ID!
  body: String!
}

input UpdateOperation {
  id: ID!
  body: String!
}

input MoveOperation {
  id: ID!
  newPos: Int!
}

input DeleteOperation {
  id: ID!
  body: String!
}

@OlegIlyenko also described similar ideas for batching multiple operations into a single atomic mutation in the context of CQRS and commerce in this blog post.

IvanGoncharov commented 6 years ago

@frikille @jturkel Thanks for more examples I will try to study them this weekend. But from the first glance, it looks like all examples are centered around mutations.

Next major question is how proposed input unions affect code generation especially for strong type languages, e.g. Java. I suspect that such languages don't support unions based on field value as the discriminator. It especially important for mobile clients. Can someone check how it will affect apollo-ios and apollo-android?

@mjmahone Maybe you know, can something like this affect Facebook codegen for iOS and Android?

jturkel commented 6 years ago

@IvanGoncharov - We have examples in the product information management space where input unions would be useful as field arguments. On such example is retrieving a filtered list of products:

input StringEqualityFilter {
  propertyId: String!
  value: String!
}

input NumericRangeFilter {
  propertyId: String!
  minValue: Int!
  maxValue: Int!
}

input HasNoValueFilter {
  propertyId: String!
}

inputunion Filter = StringEqualityFilter | NumericRangeFilter | HasNoValueFilter

type Query {
  products(filters: [Filter!]): ProductCursor
}

Given this schema customers can retrieve a list of products that meet the following conditions:

Note our customers can create their own "schema" for modelling product information so our GraphQL schema can't have hardcoded knowledge of things like brands and categories thus the more general propertyId in this example.

treybrisbane commented 6 years ago

@IvanGoncharov I agree that we should try to keep focused on practical use cases for input unions. A really big one is pretty much any mutation that leverages recursive input types.

I'm looking at implementing a GraphQL API for a pattern-matching engine that looks something like this:

type Query {
  patternMatcher: PatternMatcher!
}

type PatternMatcher {
  cases: [Case!]!
}

type Case {
  condition: Condition
  action: Action!
}

union Condition = AndCondition | OrCondition | NotCondition | PropertyEqualsCondition # ...etc

type AndCondition {
  leftCondition: Condition!
  rightCondition: Condition!
}

type OrCondition {
  leftCondition: Condition!
  rightCondition: Condition!
}

type NotCondition {
  condition: Condition!
}

type PropertyEqualsCondition {
  property: String!
  value: String!
}

# ...etc

union Action = # ...you get the idea

Naturally, I need to be able to create new Cases. Ideally, this would look something like this:

type Mutation {
  createCase(input: CreateCaseInput!): CreateCasePayload!
}

input CreateCaseInput {
  caseSpecification: CaseSpecification!
}

type CreateCasePayload {
  case: Case!
}

input CaseSpecification {
  condition: ConditionSpecification
  action: ActionSpecification!
}

inputunion ConditionSpecification = AndConditionSpecification | OrConditionSpecification | NotConditionSpecification | PropertyEqualsConditionSpecification # ...etc

input AndConditionSpecification {
  leftCondition: ConditionSpecification!
  rightCondition: ConditionSpecification!
}

input OrConditionSpecification {
  leftCondition: ConditionSpecification!
  rightCondition: ConditionSpecification!
}

input NotConditionSpecification {
  condition: ConditionSpecification!
}

input PropertyEqualsConditionSpecification {
  property: String!
  value: String!
}

# ...etc

inputunion ActionSpecification = # ...again, you get the idea

Describing this properly without input unions is impossible. The current workaround I'm using is the "tagged-union pattern", however in my experience, use of that pattern is continually met with confusion and distaste due to its verbosity and lack of clear intent.

Note that your suggestion of input Condition = { ... } | { ... } really doesn't scale here. The ability to name the branches of the input union is pretty much necessary to maintain readability in large unions. Plus, I don't see any advantages in forcing input union branches to be anonymous, especially when the same is not true for output unions.

Anyway, that's my use case. I don't really consider it a "super mutation"; it's a simple mutation that just happens to use recursive input types. :)

treybrisbane commented 6 years ago

@frikille Thanks for writing this up. :)

I definitely see why you went with the literal type. What was the rational behind going with a separate literal Foo declaration rather than just this:

input Foo {
  kind: "Foo"!
  # ...etc
}

???

frikille commented 6 years ago

@treybrisbane We were thinking about that and we think that supporting inline strings would be a nice syntax sugar but at the end, it would be parsed to a new type, so because of clarity we added the literal type (also I kinda prefer to declare everything properly)

@IvanGoncharov I'm not that familiar with Java any more, but it is a good point, and as far as I know, it doesn't have support for discriminated unions.

IvanGoncharov commented 6 years ago

I'm not that familiar with Java any more, but it is a good point, and as far as I know, it doesn't have support for discriminated unions.

Mobile is a pretty critical platform for GraphQL, so it would be great to know how it would be affected. So would be great if someone knowledgeable can answer my previous question:

how proposed input unions affect code generation especially for strong type languages, e.g. Java. I suspect that such languages don't support unions based on field value as the discriminator. It especially important for mobile clients. Can someone check how it will affect apollo-ios and apollo-android?

sorenbs commented 6 years ago

I am very happy that progress is being made on union input types πŸŽ‰

However, if I understand the proposal correctly, then it will not address the use cases we have in mind for the Prisma API.

Here is a spec for a new feature we will be implementing soon. I have written up how we would like to shape the API if union input types would support it. I have also described why the proposed solution with a discriminating field does not work for us: https://github.com/prismagraphql/prisma/issues/1349

I'm curious to hear if others are seeing use cases similar to us, or if we are special for implementing a very generic API?

Let me know if more details are needed.

dendrochronology commented 6 years ago

@sorenbs would you mind explaining your reasoning a bit more, or perhaps provide some basic query examples, please? Perhaps I'm misunderstanding your atomic actions proposal, but it seems much broader than what's being discussed here.

I think the core issue this RFC addresses is to provide a simple, declarative way to allow for multiple input types on query and mutation fields. That's a severe weakness of GraphQL when it comes to data modeling.

A lot of my recent work has been porting data from legacy relational CMS's into modern GraphQL environments, and this RFC would solve one of the biggest headaches I've had. I'd rather see the community focus on feature-lean solutions to the biggest gaps first (i.e., things without good workarounds), and focus on narrower issues in later iterations.

sorenbs commented 6 years ago

@dendrochronology - sorry for just linking to a giant feature spec, thats certainly not the best way to illustrate my point :-)

We have evolved our generic GraphQL API over the last 3 years. At 4-5 times throughout this process has it been brought up that some form of union input type could simplify the API design. Most of these cases boils down to supporting either a specific scalar type or a specific complex type:

given the types

type Complex {
  begin: DateTime
  end: DateTime
}

union scalarOrComplex = Int | Complex

type mutation {
  createTiming(duration: scalarOrComplex): Int
}

Create a timing providing duration in milliseconds or as a complex structure containing start and end DateTime:

mutation {
  simple: createTiming(duration: 120)
  complex: createTiming(duration: {begin: "2017", end: "2018"})
}

For us it is paramount that we maintain a simple API.

Did this example make it clear what we are looking to achieve?

It might also be worth noting that we have found it valuable on many occasions that A single element is treated the same as an array with a single element. This flexibility has made it easier to evolve our API in many cases, and I think we can achieve something similar for union input types if we choose a system that solves for that.

From this perspective, we would prefer the Tagged union by @leebyron and @IvanGoncharov described above. Keep in mind that I have not thought through the implications for tooling and strongly typed languages.

dendrochronology commented 6 years ago

Thanks, @sorenbs, those are great examples. I do like the simplicity, and agree that robust APIs should handle singles vs. arrays elegantly, a 'la Postel's Law.

Back to @IvanGoncharov's request for a use case-driven discussion, I think that the union type implementation needs to go a bit further than generic APIs. A lot of my work is content and UX-based, where a client application pushes/pulls lots of heterogeneous objects from a back end, and displays them to a user for viewing/interaction.

For example, think of a basic drag-and-drop CMS interface, where a user can compose multiple blocks, with some levels of nesting and validation rules (e.g., block type X can have children of types A and B, but not C, etc.). With the current state of GraphQL, you need an awful lot of client-side logic to pre/post process output, because schemas have many fields with no baked-in validation or guarantees of meeting a required contract for available data or metadata fields (e.g., all blocks should have at least a type, label, color, x and y coordinates, ordinal number, etc.). I can mockup something like that in React easily, but building a GraphQL API to power it quickly gets messy.

I suppose my key takeaway is: I find that the current GraphQL spec makes it difficult to model a lot of real-world data situations without excessive verbosity and generous additional processing. I would love to be wrong, and have someone point out what I'm missing, but that hasn't happened yet πŸ€”

jturkel commented 6 years ago

@IvanGoncharov - The proposed input union functionality for strongly typed clients should behave much the same way as "output" unions. For Java, which doesn't support unions, this can be modeled via an interface representing the union that each member type implements. Simplified code for earlier createMedia mutation would look something like:

public interface AddMediaBlock { }

public class AddPostInput implements AddMediaBlock { }

public class AddImageInput implements AddMediaBlock { }

public class CreateMediaMutation {
  public CreateMediaMutation(AddMediaBlock[] content) { }
}

// Sample code that instantiates the appropriate union member types
AddMediaBlock[] content = new AddMediaBlock[]{new AddPostInput(...), new AddPostInput(...), new AddImageInput(...)};
client.mutate(new CreateMediaMutation(content));

Presumably Swift uses a similar strategy with protocols (the moral equivalent of Java interfaces).

jturkel commented 5 years ago

@IvanGoncharov - What's your opinion on how to effectively move input unions forward? Seems like this issue has a pretty good catalog of use cases now and it's a matter of hammering out the technical details of input unions. It would be great to get a champion from the GraphQL WG to help guide this through the spec process. Is this something you have the time and desire to take on since you've already been pretty involved? I'm happy to help out in anyway I can (and I suspect others are too) but I'd like to make sure anything I do is actually helpful :)

IvanGoncharov commented 5 years ago

What's your opinion on how to effectively move input unions forward? Seems like this issue has a pretty good catalog of use cases now and it's a matter of hammering out the technical details of input unions.

@jturkel Agree and thanks for pinging πŸ‘

It would be great to get a champion from the GraphQL WG

Working group don't have fixed seats and if you want to raise or discuss some issue just add it to the agenda.

It would be great to get a champion from the GraphQL WG to help guide this through the spec process. Is this something you have the time and desire to take on since you've already been pretty involved?

I can help with the spec and graphql-js PRs but before that, we need to formulate a more concrete proposal. I thought about it for last couple months but I still see a number of blind spots.

So as a next step I think we should find some points that we all agree on. On weekends, I will try to summarize this discussion and my own thoughts in something that we could discuss.

jturkel commented 5 years ago

Great to hear you're up for helping out with this @IvanGoncharov! Figuring out where folks agree/disagree sounds like a perfect next step to me. Feel free to ping me on the GraphQL Slack if there's anything I can do to help.

IvanGoncharov commented 5 years ago

@jturkel Update: Have an urgent work so I wouldn't be able to compile something until next weekends.

dmitrif commented 5 years ago

Any updates on this? We are facing a similar need, unless there's a better way one may suggest:

input LocationQueryIdOptions {
  id: Int!
}

input LocationQueryCoordsOptions {
  latitude: Int!
  longitude: Int!
}

union LocationOptions = LocationQueryIdOptions | LocationQueryCoordsOptions

type Query {
  location(options: LocationOptions): Location
}
amccloud commented 5 years ago

@dmitrif πŸ‘ that's pretty much exactly what I tried before finding this issue:

input PostDescriptor {
  # ...
}

input ImageDescriptor {
  # ...
}

union ObjectDescriptor = PostDescriptor | ImageDescriptor;

type Mutation {
  Comment(objectDescriptor: ObjectDescriptor!, comment: CommentInput): CommentMutations
}
solussd commented 5 years ago

+1 from me. Specifically, being able to atomically add/update entities via a single mutation that have heterogeneous collections (e.g., conforming to an interface or union definition).

bericp1 commented 5 years ago

Another use case for this that we've encountered is recursive tree structures where subtrees can either be another tree of the same type as the root or a leaf type. As in:

input Tree {
  children: [Subtree!]!
  someRequiredTreeStringProperty: String!
  # ...
}

input Leaf {
  someRequiredLeafStringProperty: String!
  # ...
}

inputUnion Subtree = Tree | Leaf

type Query {
  someQuery(inputTree: Tree): SomeType
}

Currently – unless I'm missing something and short of defining a custom scalar type for our specific implementation – we have to less-than-ideally define the tree and leaf as a single type and differing fields that should technically be Non-Nullable must be left Nullable and then validated in the resolver. Detection and validation of whether or not a value in children is a Tree or a Leaf also is left up to the resolver (either by looking at the present fields or using some sort of type field):

input TreeOrLeaf {
  children: [TreeOrLeaf!]
  someRequiredTreeStringProperty: String
  someRequiredLeafStringProperty: String
}

type Query {
  someQuery(inputTree: TreeOrLeaf): SomeType
}
alfaproject commented 5 years ago

Just stumbled upon this RFC looking for a way to have a union input for an authentication API whereby I have multiple providers.

I guess for now I'll just create a generic authenticate input accepting all arguments for all providers and then validate the input fields on the back-end but it feels lame. ):

jonaskello commented 5 years ago

Our use case is "batching multiple operations into a single atomic mutation" from above. The user can make several changes then save, and we need to handle that as an atomic SQL transaction. I'm not sure how to handle a SQL transaction in the mutation resolver if we cannot get all the operations as an array input for a single mutation field. The only work-around I can think of is an array of a mega-type with all possible operations as fields, and only set one field per mega-type instance.

raderio commented 5 years ago

Our use cases are:

So, will be grate to support multi-level inheritance/polymorphism.

jhclouse commented 5 years ago

For what it's worth, here's my use case. I want to create a payment method for cart checkout. The mutation to create a payment method needs to take either a credit card or a bank account as input, but not both. So I'd like to have a single non-null field in the mutation input that represents both.

joelgetaction commented 5 years ago

I don't understand the point of having output union types but not input union types? Like why would you have one without the other? For example, suppose a user is uploading a photo with shapes drawn on top of it to annotate it? we could have:

type ShapeLine { ... }
type ShapeRectangle { ... }
type ShapeArrow { ... }
union Shape = ShapeLine | ShapeRectangle | ShapeArrow

type Photo {
  id: ID!
  url: String!
  shapes: [Shape!]!
}

input UploadPhotoInput {
  url: String!
  # The order of shapes matters because later shapes in the array will be drawn on top
  # of earlier shapes in the array
  shapes: [ShapeInput!]
}
type ShapeLineInput { ... }
type ShapeRectangleInput { ... }
type ShapeArrowInput { ... }
union ShapeInput = ShapeLineInput | ShapeRectangleInput | ShapeArrowInput

This is a perfect example of why we absolutely need union input types. If we don't have union input types then I have to have separate arrays of lines, rectangles and arrows, but because the order of their drawing matters (so that we can capture what was drawn on to of each other) I now have to patch together some artificial and clumsy system of mixed ordinals across separate arrays of ShapeLineInput,ShapeRectangleInput,ShapeArrowInput to preserve draw order or I have to create a tagged union of inputs so that I can put them in the same array and use array ordinal as draw ordinal.

Moreover, validation of separate arrays and / or tagged unions using something like Joi becomes a LOT harder without input unions ...

This simple example is made really clumsy all because GraphQL doesn't have input unions.

Any suggestions?

raderio commented 5 years ago

Swagger also has it https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/#anyof

hlminh2000 commented 5 years ago

We have a json filter DSL that was designed with some polymorphism, where a certain field may be either an object, or an array. Currently, we are having to use the JSON scalar from graphql-type-json and validate manually, which means the required shape is not apparent to the API consumer through schema inspection. This proposal would allow us to model it directly in the schema and make this structure apparent to the consumer.

Sent from my OnePlus5 using FastHub

fennecbutt commented 5 years ago

I'd find this very useful too, I've been implementing an event_name, event_payload type mutation where event_payload is polymorphic based on the value of event_name.

At the moment I'm going to have to implement each as separate mutations, or event_payload will just have be "anything" in the schema, which will mean that I need to provide additional documentation for end users.

raderio commented 5 years ago
sealed class Shape
class Circle(val radius: Double) : Shape
class Rectangle(val width: Double) : Shape

There are 2 types of polymorphism:

  1. Polymorphism by field value, aka discriminator 1.1 Discriminator embedded in object
    [ { "type": "CIRCLE", "radius": 10.0 }, { "type": "RECTANGLE", "width": 20.0 } ]

    1.2 Discriminator external

    [ { "type": "CIRCLE", "data": { "radius": 10.0 } }, { "type": "RECTANGLE", "data": { "width": 20.0 } } ]
  2. Polymorphism by filed name, aka union, or anyOf from Swagger
    [ { "radius": 10.0 }, { "width": 20.0 } ]

    Because only Circle class has radius field, first object from list will be deserialized in Circle class.

Also should be possible to do nested deserialization(multi level).

paean99 commented 5 years ago

So @raderio, trying to convert your comment to Graphql practical terms. The use of:

xmnlab commented 5 years ago

hey everyone! sorry if I am missing something related to this and previous discussions.

also I have started to use graphql some months ago .. so maybe my comments below would be very noob, so sorry about this.

I am here because we are trying to grapqhl with schema.org. Schema.org is very flexible, so for example we could have something like author: String | [String!] | Person | [Person!] | Organization | [Organization!].

maybe a little bit different about what was discussed, but what if graphql implements something a Metadata type? it could be a extensible type (user could extend that and add more fields) or this type could have a lot of defined fields that could cover a lot of use cases.

about the input union, as there is already _typename, maybe it could be used (instead of type) as discriminator or if could use a (new) Metadata type __meta.type could be used .. or also something like <template> ( not sure how it could be add in the language without break its specification).

also it could be implemented inside scalar types and we could define a string field like String(blank:False) (and this could be available using author.__meta.blank). of course it seems we can use directives to extend this capability (as SHACL does) but maybe if the language could support that in its core would be very interesting.

again, sorry if this comment is not pertinent

alamothe commented 5 years ago

@xmnlab You should do something like:

author: Author

union Author = AuthorString | AuthorStringList | Person | AuthorPersonList | ...

type AuthorString {
  value: String
}

type AuthorStringList {
  value: [String!]
}

type AuthorPersonList {
  value: [Person!]
}
...

While this might look cumbersome, it works really well in practice, and you don't need additional concepts introduced (that is, unless you need a union for an input type πŸ™‚ )

xmnlab commented 5 years ago

Thanks for the feedback @alamothe !

yes I will need union for an input type as well :)

mike-marcacci commented 5 years ago

Can we revisit exactly why a __inputname or other type marker is required here at all? I am very much of the same opinion as @jayhasyee in #415, in that the server is defining the input types in the first place and has everything necessary to disambiguate.

After all, data is just data, and there is no extra information being conveyed by a special-case __inputname parameter that can't be conveyed by a parameter defined on the data itself.

Can anyone provide a use case that requires a __inputname?

benjie commented 5 years ago

Here's a very simple example that wouldn't work without __inputname or equivalent

input PersonInput {
  name: String!
  age: Int
}

input OrganizationInput {
  name: String!
  numberOfMembers: Int
}

inputUnion ContactInput = PersonInput | OrganizationInput

type Mutation {
  addContact(contact: ContactInput!): Contact
}
mutation {
  addContact(contact: {name: "IBM"}) {
    __typename
    name
  }
}

If we require users to add a special property to the input types to disambiguate them (we called this a "discriminator" above), why should it not be a single standardised explicit __inputname?

xmnlab commented 5 years ago

hey everyone, not sure if it is relevant, but I could resolve my issue related to union of input type using https://github.com/Cardinal90/graphql-union-input-type with __typename as discriminator

mike-marcacci commented 5 years ago

@benjie I disagree with that – I did agree at first, but when I really think about why GraphQL has a __typename in the first place, I am re-convinced that __inputname doesn't belong.

My understanding of the GraphQL type system is that it is primarily structural as opposed to nominal, and that type and interface names exist for very pragmatic reasons given the relationship between the server and the client:

The server is responsible for exhaustively defining the types used by the API; the client is primarily concerned with fetching the data that it needs and should not need to be concerned with the presence of types or fields that it doesn't want. Let's take a modified version of your example:

type Person {
  name: String!
}

type Organization {
  name: String!
  members: [Person]
}

union Contact = Person | Organization

type Query {
  contacts: [Contact]
}

If I'm a client with a query like this:

query {
  contacts {
    name
  }
}

...then there's no way for me to tell the difference between a Person and Organization without querying a potentially huge field, which I may not want. Because I'm the client and not the server, I don't have the ability to add a field to the schema for disambiguation! This is why __typename is so important: because it gives the client a way to resolve types without over-querying.


Now let's look at the dynamics of input polymorphism from the server's perspective:

input PersonInput {
  name: String!
}

input OrganizationInput {
  name: String!
  memberIds: [ID!]
}

input union ContactInput = PersonInput | OrganizationInput

type Mutation {
  addContact(contact: ContactInput!): Contact
}

In this case, the server is unable to distinguish between a PersonInput and an OrganizationInput if the latter is sent without memberIds. However, this schema should simply never be written! The server gets to specify its data requirements by writing the schema just like the client specifies its data requirements by writing the query. In this case, we've simply written a bad schema: this is identical to a query failing to request a __typename in its query when it needs one. It doesn't get one because it didn't ask for one! The solution here is simply to add a required enum type field onto each input – or any other kind of field that would be relevant.

I hope this makes sense; adding __inputname is not only unnecessary, but really ugly, and in my opinion muddies the conceptual relationships between the server/schema and client/query pairs.


On a related note, don't be fooled into thinking that __typename comes without its own catches: a huge advantage of GraphQL's primarily structural type system is that you can start by defining a concrete type Contact, which is generally safe turn it into an interface later on; when clients use __typename, however, this becomes a breaking change, since there is no longer a Contact but a Person and Organization.

treybrisbane commented 5 years ago

@mike-marcacci

My understanding of the GraphQL type system is that it is primarily structural as opposed to nominal

Can you explain how you arrived at this conclusion? Because I'm not sure it's correct...

Let's assume you're right and that GraphQL's type system is indeed structural. Under that assumption, say we have the following schema:

schema {
  query: Query
}

type Query {
  fooBar: FooBar!
}

type FooBar {
  name: String!
}

Such a schema can be queried like so:

query {
  fooBar {
    name
  }
}

If GraphQL's type system is structural, we should be able to change the definition of FooBar to be any other structurally equivalent type without having to change the query. Suppose we change the schema to this:

schema {
  query: Query
}

type Query {
  fooBar: FooBar!
}

union FooBar = Foo

type Foo {
  name: String!
}

Unfortunately, in this schema, FooBar is not Foo despite being structurally equivalent; it is a distinct type unto itself. This is evident by the fact that you must use an inline fragment to access the name field of a FooBar in a query:

# Error!
query {
  fooBar {
    name
  }
}

# We must do this instead
query {
  fooBar {
    ... on Foo {
      name
    }
  }
}

Thus, we have a contradiction, and so GraphQL's type system cannot be structural in nature.


I'm not sure I follow the rest of your argument, but one thing did stick out to me:

...However, this schema should simply never be written!... ...In this case, we've simply written a bad schema...

How do you define "bad" in your example case?

The reason I ask is because I've seen numerous valid use cases for explicitly discriminated input unions across three (including this one) separate input union threads. Are you suggesting that such use cases are somehow wrong?

mike-marcacci commented 5 years ago

@treybrisbane thanks for the thoughtful reply. While this is not essential to my argument against __inputname, I think I might not have adequately described the ways in which GraphQL is structural vs nominal. GraphQL is structural in that the type system is specifically designed to make the action of subdividing a type into multiple types a backwards-compatible change.

Here I'll elaborate on the example I used in the final notes above.

Say I have a concrete type in my schema:

type Contact {
  name: String!
}

My app becomes really successful, but we don't have the ability to immediately update all clients, because many are native apps, which users update at different paces. This makes it important that all our API changes are backwards-compatible.

However, we realized that we need to separate our Contact into separate Person and Organization types. Fortunately, GraphQL can handle this! Our Contact can become an interface that is inherited by the new types:

interface Contact {
  name: String!
}

type Person implements Contact {
  name: String!
}

type Organization implements Contact {
  name: String!
  members: [Person]
}

A more concise example:

type Foo {
  hello: String!
}

type Query {
  foo: Foo!
}

can easily be replaced with:

type Bar {
  hello: String!
}

type Query {
  foo: Bar!
}

without breaking any queries, because the structure is our primary concern.


All that said, you are completely right that type names play an important role, and your example with a union is perfect. However, as I attempted to describe in my previous comment, I believe this is for the practical purpose of allowing the client to disambiguate without requesting additional information. This is because the client states its data dependancies by writing the query. On the other side, the server states its data dependancies by writing the schema.

A query or schema is "bad" when it doesn't request the data it needs.

I think there is definitely a need to discriminate between input union types; I just think that this can be solved in the schema itself, as opposed to the spec.

mike-marcacci commented 5 years ago

As a counter to my own argument, I can imagine some scenarios where using an __inputname could be far more ergonomic: it allows the same input type to be used both inside and outside a union, without adding requiring disambiguating info when it isn't needed, but requiring it when it is needed.

input CatInput {
  name: String!
  lives: Int
}

input DogInput {
  name: String!
  fetches: Boolean
}

input union PetInput = CatInput | DogInput

input PersonInput {
  name: String!
  pets: [PetInput]
}

type Mutation {
  addPerson(person: PersonInput): Person
  adoptDog(personId: ID!, dog: DogInput): Person
  adoptCat(personId: ID!, cat: CatInput): Person
}

Without the spec-defined __inputname, Dog and Cat would have to be written twice – one set with disambiguating characteristics and one without. This is a pretty compelling case for me, so I fully retract my objections!

gustavopch commented 5 years ago

I had a case where I thought input unions would be needed, but I found a solution that's neither awkward nor requires changes to GraphQL spec. So I thought I should share my experience:

It's an e-commerce. Each order has an array of items. Each item can either be a product item (represents a single product) or a combo item (represents a combination of products usually with a discount). They have different structures. When submitting an order, I thought I needed input unions so that I could have something like this:

inputUnion OrderItem = ProductItem | ComboItem

input OrderCreationParams {
  items: [OrderItem!]!
}

But I ended up doing this:

input OrderCreationParams {
  productItems: [ProductItem!]!
  comboItems: [ComboItem!]!
}

Then I just needed to merge productItems and comboItems in the resolver (using JS here):

createOrder: async (parent, { params: { productItems, comboItems, ...rest } }) => {
  const params = { ...rest, items: [...productItems, ...comboItems] }
  ...
}

Simple and ellegant IMHO.

So I think it should really be analyzed whether the complexity that will be added to GraphQL by having input union types is really worth. Note that I just needed 1 extra line in the SDL and 1 extra line in the resolver to accomplish what I needed.

Also, it seems the case showed in the original post could easily be solved with this SDL:

type Mutation {
  createMedia(addMediaInput: AddMediaInput, addPostInput: AddPostInput): Media
}

And this resolver:

createMedia: async (parent, { addMediaInput, addPostInput }) => {
  const input = addMediaInput || addPostInput
  if (!input) throw new Error('No input specified')
  ...
}

How awkward is that? Or did I miss something?

binaryseed commented 5 years ago

That works when you can accept a list of items, but it's not possible to use this when you can only accept a single item...

gustavopch commented 5 years ago

That works when you can accept a list of items, but it's not possible to use this when you can only accept a single item...

Why? Look at my second example. It splits what would be a single argument into two and easily solves the problem.

mikeplis commented 5 years ago

When you split a list of heterogenous types into multiple lists of homogenous types, you lose the original ordering of items. If that ordering is not significant to your use case, then it's fine, but that's not going to be the case all the time.

gustavopch commented 5 years ago

When you split a list of heterogenous types into multiple lists of homogenous types, you lose the original ordering of items. If that ordering is not significant to your use case, then it's fine, but that's not going to be the case all the time.

That's a good point. But would it be too much to require the client to pass some sort of an index property that would be used to reorder the merged array if the order matters?

createOrder: async (parent, { params: { productItems, comboItems, ...rest } }) => {
  const items = [...productItems, ...comboItems].sort((a, b) => a.index - b.index)
  const params = { ...rest, items }
  ...
}

EDIT: I mean, look at the proposal. It also adds complexity and new lines of code.

mikeplis commented 5 years ago

But would it be too much to require the client to pass some sort of an index property that would be used to reorder the merged array if the order matters?

Whether it's too much to ask likely depends on the use case, but I think it's definitely more awkward for the client than being able to send all the items in a single list.