Closed frikille closed 4 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:
description
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.
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 ofPost
. 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.
@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?
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.
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.
@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?
@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.
@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 Case
s. 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. :)
@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
}
???
@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.
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?
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.
@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.
@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.
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 π€
@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).
@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 :)
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.
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.
@jturkel Update: Have an urgent work so I wouldn't be able to compile something until next weekends.
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
}
@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
}
+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).
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
}
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. ):
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.
Our use cases are:
So, will be grate to support multi-level inheritance/polymorphism.
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.
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?
Swagger also has it https://swagger.io/docs/specification/data-models/oneof-anyof-allof-not/#anyof
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
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.
sealed class Shape
class Circle(val radius: Double) : Shape
class Rectangle(val width: Double) : Shape
There are 2 types of polymorphism:
[ { "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 } } ]
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).
So @raderio, trying to convert your comment to Graphql practical terms. The use of:
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
@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 π )
Thanks for the feedback @alamothe !
yes I will need union for an input type as well :)
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
?
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
?
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
@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
.
@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?
@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.
__typename
in the response. A query that fails to do this is a "bad" query, and it would naturally be difficult to use one.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.
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!
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?
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...
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.
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.
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.
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.
[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 @tgriesserRFC document: #395
Reference implementation: graphql/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.Tagged union by @leebyron and @IvanGoncharov
Original comment
Directive
Original 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:
With that our GraphQL schema definition would be the following (Full schema definition)
And a mutation query would be the following:
or with variables
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.
Pros
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.