-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: inputUnion type #395
RFC: inputUnion type #395
Conversation
Thanks for taking the time to put this PR (and the associated A couple of things on this:
Given that when querying a union type we must discriminate using specific syntax (i.e. a conditional fragment), it would make sense if we could somehow employ a syntactic approach to discriminating inputs. One example is:
This would give you the ability to discriminate without including a magic field (GraphQL could pass on the "as X" type name to resolvers via the
The above is just an example, but the takeaway is that it would make sense to discriminate inputs via some syntactic construct if possible, given that that's how we discriminate outputs. Alternatively, if we can't (or don't want to) do things with syntax, then we should probably consider a way to make the name of the discriminant field configurable. Thoughts? |
Yes. Most of the comments in the related PR's and tickets mentioned wanting to re-use
I don't know that it's necessarily reserved for introspection, that was sort of an open question. In querying it is reserved for introspection but I'd imagine it'd be reasonable to assume I wouldn't focus on the ugliness, as queries utilizing input unions are unlikely to be written by hand in the query string.
Outputs are generally discriminated at runtime via
Also, in your example above, you lose the ability to have a list of heterogeneous input types, which is one of the big wins of this addition.
I would disagree, I think this would be better standardized than configurable. |
Thanks for the response! :)
Sounds fair enough to me. :) Yeah, the more I think about it, it seems like it may have been better for
Ahh, you're absolutely correct. I admit I had not considered that case. Thanks for pointing that out. :)
You're correct in that Since we're already looking at adding new syntax (i.e.
Or
You could potentially even use the directive syntax here (defaulting to
Again, the syntax is open to change here, but I think being able to somehow specify this in the schema is important. I can understand where you're coming from with regards to standardisation over configuration, but we won't have anything to standardise if we can avoid using magic field names like |
Would it not be possible to still use We would need a little lookahead in the parser to see if |
@Yogu Sounds reasonable! I don't really have a strong opinion here.
@Tbrisbane I don't believe |
types are not permitted in query results so these types should be grouped separately. | ||
|
||
`Input Union` types may also be used as values for input objects, allowing for | ||
arbitrarily nested structs. In order to determine the concrete `Object` type fulfilling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should say:
the concrete
Input Object
type
Thanks for drafting this up, Tim! This is a really interesting approach to this problem. In seeking a comparison point to draw parallels, pros and cons I think this tagged-union pattern is closest to your initial example: input PostInput {
title: String!
body: String!
}
input ImageInput {
photo: String!
caption: String
}
# Note: this is the only change from the example type system.
# A server may wish to add dynamic enforcement that one field is provided
input MediaBlock {
postInput: PostInput
imageInput: ImageInput
}
type Mutation {
addContent(content: [MediaBlock]!): Post
}
mutation AddContent($content: [MediaBlock]!) {
addContent(content: $content) {
id
}
} Which would have the corresponding valid [
// Note: no need for a special reserved string key, but contains semantically
// similar content as example.
{postInput: {title: "Hello", content: "World"}},
{imageInput: {photo: "http://graphql.org/img/logo.svg", caption: "Logo"}}
] In looking at the above, I'd like to challenge your answers to "Does this enable new use cases?" and "Can we enable it without a change to GraphQL?" Can you help clarify what new possibilities or use cases are enabled by |
Thanks for the feedback, Lee!
That is exactly the trade! However I view the complexity from a different perspective. Implementing these sorts of dynamic checks ad-hoc during execution (as you mention would be warranted in the comment for Should The example you provide requires wrapping each input value in an additional input object. When creating input shapes more complex than this contrived example (nested and otherwise), it feels like an antipattern to need to emulate unions in this fashion, for lack of a better built-in alternative. To think of a parallel on the query {
contentBlocks {
postContent {
title
content
}
imageContent {
photo
caption
}
}
} While it initially appears more complex, in practice it's much simpler for tooling, type generation, and end user code to utilize query {
contentBlocks {
__typename
... on Post {
title
content
}
... on Image {
photo
caption
}
}
} |
I like the idea of allowing input objects to be a more flexible but still be able to fully validate it.
I'd like to propose an alternative solution:
So the idea is to allow define input object as disjointed sets of fields with the following requirements:
It not only adds type safety to tagged-union pattern suggested by @leebyron above but also allows to eliminate it if your sets of fields don't have conflicting fields:
We can make it a non-breaking change for client tools by extending type __Type {
kind: __TypeKind!
# ...
# INPUT_OBJECT only
inputFields: [__InputValue!] @deprecated(reason: "Use `inputFieldSets` instead")
# INPUT_OBJECT only
inputFieldSets: [[__InputValue!]!]
} Old clients will continue query for input MediaBlock {
postInput: PostInput
imageInput: ImageInput
} New clients will use @tgriesser What do you think about this proposal? |
I would respectfully disagree on both of these assertions. Being explicit about which input type member we mean seems like it could only yield better DX, particularly when combined with TypeScript & Flow generated types. Thinking of autocomplete for graphiql, once the I also wouldn't say the learning curve is steep, to the extent that Further, I would say the learning curve would be eased by helpful errors are provided when
Again, only if you utilize this new type, which is not required to have a working GraphQL query. If the const PostInput = (a) => ({ ...a, __inputname: 'PostInput' })
const ImageInput = (a) => ({ ...a, __inputname: 'ImageInput' })
AddContent([
PostInput({ ... }),
ImageInput({ ... })
]) I suspect most uses of this type would have some sort of abstraction built around this and hand-written mutations with
I'm interested to hear more about this. As it's an additive change (new type), I can't imagine it would break too much existing tooling, at least no-more so than a potential change to the structure of
That's actually one of the big motivations here, and I think would be a non-starter. Trying to consolidate: mutation {
updateAccountBasic(input: UpdateAccountBasicInput!): Account!
updateAccountContact(input: UpdateAccountContactInput!): Account!
updateAccountFromAdmin(input: UpdateAccountFromAdminInput!): Account!
updateAccountPreferences(input: UpdateAccountPreferencesInput!): Account!
} into: inputUnion UpdateAccountInput = UpdateAccountBasicInput | UpdateAccountContactInput | UpdateAccountFromAdminInput | UpdateAccountPreferencesInput
mutation {
updateAccount(input: UpdateAccountInput!): Account!
} Where all of these inputs contain an Yes, we could do: input UpdateAccountInput = {
basic: UpdateAccountBasicInput
contact: UpdateAccountContactInput
fromAdmin: UpdateAccountFromAdminInput
preferences: UpdateAccountPreferencesInput
}
mutation {
updateAccount(input: UpdateAccountInput!): Account!
} But now we've lost the type safety and intention of the API, added an additional But with all that said, I'm not necessarily convinced on tagging the input with |
Given the suggestion that this problem can be worked around by using composition instead of subtyping, I'd pose the question: is I think it's totally reasonable either way to choose a richer language for data modeling or sticking to a small set of core building blocks. But it seems strange to me to make different choices for input and output type modeling. Unless there's a distinction I'm missing. |
I suppose another option is to indicate usage with a custom directive (is this valid syntax/how directives should be used?): input UpdateAccountInput @inputUnion {
UpdateAccountBasicInput: UpdateAccountBasicInput
UpdateAccountContactInput: UpdateAccountContactInput
UpdateAccountFromAdminInput: UpdateAccountFromAdminInput
UpdateAccountPreferencesInput: UpdateAccountPreferencesInput
} And then any type-gen utilities could key off the presence of the type UpdateAccountInput =
| { UpdateAccountBasicInput: UpdateAccountBasicInputType }
| { UpdateAccountContactInput: UpdateAccountContactInputType }
| { UpdateAccountFromAdminInput: UpdateAccountFromAdminInputType }
| { UpdateAccountPreferencesInput: UpdateAccountPreferencesInputType }; Still feels like a suboptimal solution, but I think I'd take it over the alternate syntax for |
@tgriesser Using directive is a great idea and definitely the safest path 👍 input UpdateAccountInput @inputUnion {
basic: UpdateAccountBasicInput
contact: UpdateAccountContactInput
fromAdmin: UpdateAccountFromAdminInput
preferences: UpdateAccountPreferencesInput
} Bikeshedding: I like |
For what it's worth, I strongly disagree with any reliance on the so-called "tagged-union pattern", even if it's extended with directives. It's very easy for us to use contrived and simplistic examples of the tagged-union pattern to justify not adding first-class language support for input unions, but in practise, my experience has shown that this pattern does not scale. As is the case with any language, we should definitely be conservative in regards to adding features to GraphQL. But we need to be careful that we don't "err" too hard on the conservative side and risk dismissing features that stand to solve an ongoing problem for GraphQL's users. I see the current state of affairs as this:
Given the above, I think we have just-cause for adding first-class language support for input unions. Or, to put it another way, I don't think that the value of input unions is low enough to justify continued reliance on patterns/workarounds in lieu of first-class language support, especially when said patterns don't scale. |
Regarding the proposals and discussion thus far, I see @tgriesser 's original proposal as having the following pros and cons:
Additionally, I see @IvanGoncharov 's alternative proposal as having the following pros and cons:
Finally, I see @tgriesser 's
Based on all this, it is clear to me that @tgriesser 's original proposal is by far the best option so far. So, if that proposal isn't good enough in it's current form, my question is this: What do we need to change in the original proposal to make it acceptable? |
@IvanGoncharov that was intentional, the goal being to eliminate needing yet another name to express something, by just duplicating the input type name. I would want to sidestep questions of:
All things that a @Tbrisbane Well summarized
If I understand correctly, users can now define custom directives in the schema definition language. My idea there was if this were to fall to being implemented in userland, that's a possible solution without needing any language extension (pro and con). I would still love to see something baked-in for all of the reasons you mentioned, but if there isn't a consensus I'd understand as well. |
Hey guys, has there been any further developments on this? I currently maintain two GraphQL APIs, and this continues to be a pain point for me. In the interests of moving forward with this, can people concisely list out any specific things from @tgriesser's proposal that they think need either changing or further thought? |
For me, I have reduced that list to only a single minor item:
We could argue in favour of In a similar vein, we could argue in favour of Honestly, I'd be happy with either of those. I'm just not a fan of a camel-cased keyword, given that most languages seem to avoid this (e.g. Other than that, I think Tim's proposal is sound, despite my earlier misgivings around |
This is the biggest issue that I have with my GraphQL API. I end up having to use unions on outgoing calls and JSON scalars on incoming calls (as a bad work-a-round). For me the biggest benefit of GraphQL is schema validation and the API contract that develops between client and server. Not using polymorphic/union capability results in huge API bloat or really inefficient data structures where the front-end ends up having to manage multiple strongly typed data fields which are all set to null bar one. I fully support @tgriesser approach and would prefer using lowercase union and __inputname keywords. The increase in syntax complexity is insignificant and actually makes GraphQL clearer and far richer IMHO. I would also like to see the overlapping field restrictions on unions and input unions dropped. Please, lets get this finalized soon!!! |
@tgriesser maybe you (or someone else) would love to join for the next GraphQL working group meeting to discuss how to move forward with this proposal? |
Is there any timeframe for when we can expect input unions to make it into the draft specification and when the draft specification is expected to be finalized? Would it be possible to create a roadmap file for this and other upcoming changes? |
It would be nice if changing an input field from an input object type to an input union type was backwards compatible since it's often hard to predict where polymorphism will be required in a data model. Given that, should we allow any input object (i.e. not just an input union member) to optionally include the |
Or just allow to specify a default union option that is assumed by the
server when __inputname is missing.
Joel Turkel <notifications@github.com> schrieb am Do., 3. Mai 2018, 22:48:
… It would be nice if changing an input field from an input object type to
an input union type was backwards compatible since it's often hard to
predict where polymorphism will be required in a data model. Given that,
should we allow any input object (i.e. not just an input union member) to
*optionally* include the __inputname attribute? The recommendation would
be that client side tooling always include the __inputname argument to
better support schema evolution even if it's only required for input unions.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#395 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAYKIj7tsKni2VsuPvVWDx7mN1YubODoks5tu20ggaJpZM4RbYsm>
.
|
Yesterday I compared Now, I absolutely agree with Anyway, if GraphQL type system was closer to TypeScript/Flow/OtherTypedLang, and do not provide additional According to the current differ output/input types by GraphQL design – the |
Honestly, inputName, typeName, whatever you want, just get into a
conclusion in this meeting and let this be implemented as soon as it can
|
Yesterday there was discussion of creating a work draft in the repo similar to what was done for subscriptions. I think this would help unify all the proposed solutions. @tgriesser Your solution proposed here seems to be the closest to a consensus. @leebyron and others had concern about requiring a descriminator property (vs inference + ordered list of types), but having one RFC for everyone discuss would create a forum for this discussion. Does it make sense to create |
I totally agree with @jodinathan that any solution would be appreciated. @asprouse Could a compromise between discriminator property and a more organic solution be that the discriminator property could be configurable. It could be:
That would combine the fast resolve of the discriminator with the adaptability of inference. |
Since the Working Group meeting, I've been pondering @leebyron's challenge to see if we can design an Most of my thinking so far has been about the "runtime" behavior of the schema, but if we step back - I think there might be a "compile time" rule we can put in place that makes this a whole lot simpler. This would be a rule we enforce before a schema is even considered valid. Schema Rule:
With this rule in place, I believe it might be possible to do structural type discrimination w/o relying on any of the previous band-aids ( With this simple rule, we pretty much get everything we want: a nice schema definition with minimal SDL changes (a single new keyword), no type artifacts in the query, and deterministic behavior relying on nothing more than the schema. inputUnion MetricInputUnion = SummaryMetric | CounterMetric | HistogramMetric
input SummaryMetric {
name: String!
min: Float!
max: Float!
count: Integer
}
input CounterMetric {
name: String!
count: Integer!
}
input HistogramMetric {
name: String!
percentiles: [Integer]!
width: Integer
}
{name: "response.time", min: 123.4, max: 456.7, count: 89}
{name: "http.requests", count: 789}
{name: "response.time", percentiles: [12, 34, 56, 78, 90]} If you wind up wanting two types that match exactly, this rule would push your design to contain enough fields to properly resolve the types. As far as I can tell, that's a good thing. The auto-complete story isn't quite as simple as the rest of GraphQL, but it's definitely possible. To start, the interface could display a list of the possible fields grouped by type. As a field is chosen, the list of possible types is simply narrowed to those that still match. The only downside I can currently think of is that sometimes you might actually want to have a field that maps 1-1 to the type, like an {metricType: SUMMARY, name: "response.time", min: 123.4, max: 456.7, count: 89}
{metricType: COUNTER, name: "http.requests", count: 789}
{metricType: HISTOGRAM, name: "response.time", percentiles: [12, 34, 56, 78, 90]} Probably not that big of a deal. Does anyone see any other issues that might arise with this solution? |
@binaryseed 1) Wouldn't this fall apart in the case where only the shared field was being provided? 2) there is value in separating types for logical reasons and future-proofing even if different types have the exact same fields. For example, |
This would have to be structurally recursive to be effective. I can imagine some not-so-nice scenarios in which a deeply nested input type ends up being the "deciding type" for an input union many layers above, leading to potentially very confusing breakages when such types are changed. Not sure how likely that kind of scenario is in practice though. In any case, what you're suggesting would not address my particular use case, which is representing an AST in GraphQL: input AndExpressionInput {
lhs: ExpressionInput!
rhs: ExpressionInput!
}
input OrExpressionInput {
lhs: ExpressionInput!
rhs: ExpressionInput!
}
# ...more types...
inputUnion ExpressionInput = AndExpressionInput | OrExpressionInput | # ...more types...
More generally though, such an approach would also still leave us with an inconsistency between input and output unions. I personally see resolving this inconsistency as one of the primary benefits of this proposal. |
Potentially crazy idea: Only require Has a big downside in that adding a new structurally-identical type to an input union may break clients though, so probably not a good option. |
I don't think so. The rule is that there is a unique set of required fields, so there can't be a case where only "shared" fields are provided...
I don't see why this is true, can you explain? My proposal only states that a unique set of required fields is present, it doesn't say anything about what the types of those fields can be. |
@binaryseed - I believe your algorithm needs to take optional fields into account. See my comment for more details. |
My interpretation of "field" is a "name, type pair". 😄 So the following would be a valid schema, because even though both input Foo {
id: String!
}
input Bar {
id: Int!
}
inputUnion FooBar = Foo | Bar More generally, my interpretation was that we would need to include both the name and the type of each field in an input object type when comparing branches of an input union (in order to fully know if branches are identical). As a consequence of including field types in the comparison, the comparison becomes recursive, as we need to be able to determine whether two fields that have identical names but different input object types are equal. input FooData {
name: String!
}
input BarData {
name: String!
}
input Foo {
data: FooData!
}
input Bar {
data: BarData!
}
inputUnion FooBar = Foo | Bar Am I correct in assuming you're talking about only the field names? If so, under your proposal, neither of those above schemas would be valid, but I'd consider that to be very restrictive. |
What about the case of nullable(optional) value types, for example int that have null value?
in this case the only way to do figure out a difference is provide a function which can tell for a required field which value type should be expected |
I created a PR to start a doc for further detailed discussion about possible solutions: |
I think we should revisit the tagged union pattern, discussed at the beginning of the thread, with the addition of a directive to require exactly one field be specified. To help with this, I've written it up as a straw-man: #586 |
@benji Presumably you're suggesting this because you dislike one or more aspects of the current proposal... Can you elaborate on why you don't feel that the original proposal is worth pursuing? From memory, I don't recall that approach having much (if any) substantive support during this discussion, so I admit to being a little confused as to why you feel we should revisit it. :S |
Back when tagged unions was first proposed, there was a lot of hope that we’d find a clean solution to input unions that pleased everyone. Alas we have not; people are hung up (and rightly so, IMO) on Tagged input unions already work, they just lack a tiny feature that would make them type safe. I’ve read through some of the disagreements and there seems to be a lot of hand-wavey things, like claims of decreased performance, aesthetics, etc. I cannot see how it would be any less performant than searching a chain of possibilities at runtime to determine the type. Further, it enables the scalar/object alternatives that a few people have wanted, that inputUnion cannot seem to give us. Visiting the guiding principles of GraphQL spec changes, tagged unions with an added directive to enable type safety ticks more of the boxes than inputUnion; it achieves the goal with significantly less implementation overhead, is much more backwards compatible, and almost negligible performance overhead. As the guidelines say: “There are plenty of behaviors and patterns found in other languages intentionally absent from GraphQL. "Possible but awkward" is often favored over more complex alternatives.” I believe, now that we know our options for inputUnion, we should revisit this enhanced proposal and evaluate it critically versus inputUnion, keeping in mind that there’s nothing stopping us adding actual input unions in future should we so desire. I truly believe that the tagged union pattern with added directive for type safety is the best option available to us currently. |
As I mentioned in the WG meeting, here's a general utility script I created tonight that handles building all the linkages for you, if anyone wants to iterate on another fork of the https://www.npmjs.com/package/yarn-compose the readme demonstrates a config for re-creating the setup I described above, using the same branches that were used in the graphql working group demo. |
well put @benjie, I like this proposal a lot, and not just because of how simple it'll be to implement for various runtimes and for the IDE ecosystem(s). I'm torn between this and the discriminator oriented proposals personally, but I'm glad to help advance whatever proposals folks want to prototype and/or officially adopt in the long run. I just wanted to say, this has been a really amazing effort so far. We have the opportunity to learn from prior art and the mistakes of other API standards. Whatever comes of this deliberation and RFC process will open up so many new possibilities in the GraphQL community! |
Hello! What is the current state of this RFC ? Is it waiting for something ? Thanks! |
@bilby91 looks like it was merged into the RFCs in graphql-spec repo: https://github.com/graphql/graphql-spec/blob/master/rfcs/InputUnion.md |
Sorry, but does this mean it is official now? |
The files in RFC are collaborative RFC. Since multiple people have been contributing it was merged into that folder so additional people can work on it. It’s “official” (final stage) once it has been merged into the specification document itself. The last two working group meetings have not had agenda items from champions for this RFC, so I think you should reach out to the RFC authors to offer your help if you would like to see things move quicker. |
We have begun building a single RFC document for this feature, please see: The goal of this document is to consolidate all related ideas to help move the proposal forward, so we decided to close all overlapping issues including this one. If you want to add something to the discussion please feel free to submit PR against the RFC document. |
For anyone trying to follow that (now broken) link: the latest discussion on this topic is #825 👍 |
Related graphql-js PR: graphql/graphql-js#1196
This is an RFC for a new type:
inputUnion
.An
inputUnion
is a union of one or moreinput
types. It may be used in any location where aninput
is currently valid. When fulfilling aninputUnion
an additional field__inputname
must be specified in the map/object fulfilling the input, where the value of__inputname
is the name of a single member of theinputUnion
being fulfilled.Example:
Valid
$content
value:Invalid Value Examples:
Checklist:
Are we solving a real problem.
Yes. Many of these problems or use cases are laid out in graphql/graphql-js#207 but to summarize:
When creating input objects, both in mutations and queries you face a tradeoff when creating complex input structs, with one of two options:
!
. Create multiple endpoints (mutation or query) utilizing these various strict, special case input types.This solution aims to offer a third path, where more complex combinations of strict input combinations may be utilized, while still keeping the input types fulfillment unambiguous via the
__inputname
field requirement.Does this enable new use cases.
Yes. Many of the use cases are detailed in graphql/graphql-js#207. I think the biggest thing this unlocks is the list of heterogeneous inputs, which can be used to define an ordered set of instructions. This also reduces the need for many individual mutations while being able to maintain strictly typed inputs. In my experience tools like apollo-codegen and graphql-code-generator have proven invaluable in creating Flow/TypeScript definitions for validating queries. This change will work well in combination with those tools, making complex input semantics simpler to statically check.
How common is this use case.
Very common. This is the most commented issue in graphql-js, and I personally have run into the tradeoff of creating many highly restrictive mutations vs loosening them up and creating an ad-hoc server implementation. This sort of concept feels like the missing corollary to the expressiveness of the graphql Query execution (single "smart" entry point rather than many ad-hoc endpoints).
Can we enable it without a change to GraphQL.
No, at least not without pushing any type-checking semantics to the execution layer.
Additional thoughts
What about
interfaces
? There are several comments in related tickets expressing a desire for interfaces in addition to input unions. While it sounds nice for symmetry with querying, I don't see these as being useful or necessary in practice at the input layer. Interfaces are most useful when you wish to query for a generic partial representation of a more specific type. This same requirement does not exist for inputs and it is my opinion thatinputInterface
would not add enough additional value to justify its addition.Open questions:
__inputname
a valid option based on spec (__ is reserved for introspection, not sure if we can mirror this for execution)__inputname
make sense as the name for this?