-
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
Proposal: Support union scalar types #215
Comments
Could you share a couple of concrete use cases? I ask because, personally, I'm usually reading values right out of MySQL (or running those values through transformations with predictably-typed output), so I haven't needed this yet! |
Here is a concrete use case that we've come across at my team. I have a couple more if you need them. We've got an algorithm that produces results based on location. We have an enum If we could write scalar unions then it would solve this problem for us, among others (we would like to have constructs like |
In our case, it's because we were returning a structure that represented a JSON tree, where keys can be integers (in the case of arrays) or strings (in the case of objects). We ended up compromising for now to just use strings, but I can imagine how this would be extra useful in the case where you want to return one of a set of custom scalars, which might have different serialization logic. |
Here are other relevant discussions: |
Allowing uniontypes to contain primitives allows us to model any kind of source that can be polymorphic in its source data. For example, avro data or elasticsearch. This is a severe limitation on exposing existing data, rather than creating new systems that are defined by graphql from the ground up. |
What's still unclear to me is what a valid query would look like which encounters such a union, and how should well-typed clients determine what to expect from such a query? Consider: type MyType {
field: String
}
union MyUnion = String | MyType
type Query {
example: MyUnion
} |
Like with any union - it has to be prepared for any item in the union. I
don't see that this poses any special problems.
…On Wed, Feb 8, 2017 at 7:52 PM Lee Byron ***@***.***> wrote:
What's still unclear to me is what a valid query would look like which
encounters such a union, and how should well-typed clients determine what
to expect from such a query?
Consider:
type MyType {
field: String
}
union MyUnion = String | MyType
type Query {
example: MyUnion
}
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#215 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABLqONw9c-XUFzYcBYSTtPU2GGBsr5J7ks5ramNMgaJpZM4KLagd>
.
|
For the above schema consider the query: { example } It's clear what this query should return if the Similarly: {
example {
... on MyType {
field
}
}
} It's clear what this would return for |
The exact same thing as querying a value that doesn't exist on a member of
a union.
…On Wed, Feb 8, 2017 at 8:36 PM Lee Byron ***@***.***> wrote:
For the above schema consider the query:
{ example }
It's clear what this query should return if the example field returns a
String at runtime, but what should it return if the value was a MyType?
Similarly:
{
example {
... on MyType {
field
}
}
}
It's clear what this would return for MyType, but what should it return
for a String?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#215 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ABLqONSR3whAYxVVrPBjdCmGOT_Py57tks5ram2rgaJpZM4KLagd>
.
|
It's not clear to me what that means. What would a valid query look like that would clearly give a value for a primitive type or an object type? |
Specifically http://facebook.github.io/graphql/#sec-Leaf-Field-Selections is what I'm referring to as "valid query" |
I think the original issue is about having unions where every member is a scalar right? Not unions between scalers and objects? |
just an idea. Maybe it would make sense to introduce special inline fragment semantics for scalars: {
example {
... on String {
value
}
... on MyType {
field
}
}
} This can be generalized by allowing scalars to be used as leafs (current semantics) and as objects with a single field ( In our API we have a similar issue. For some parts of the schema, we even have introduced object types like |
What if the result was a scalar then the result would simply ignore the selection set, and if it is an object type then it would consider the selection set. Statically, the query can be analyzed because the schema should have enough information to know if the union contains a scalar or an object or both. If there is no selection set and the value is an object then the returned value can just be On the client most type system allow unions of arbitrary types, so the client can expect a result that is Nevertheless, this starts getting into the design choices of the simplicity of GraphQL, which I think @leebyron made great points here: graphql/graphql-js#207 (comment) |
Mixing scalars and object types would require a change to the schema which is too complex to consider. Schemaless GraphQL clients depend on the guarantee the type StringBox {
value: String
} |
@calebmer, agreed! It's a balance between feature-set and simplicity and I think the GraphQL designers wanted to err on the side of simplicity. Please correct me if I'm wrong @leebyron. Given this, I think that the original discussion of scalar-scalar (or more specifically, leaf nodes) unions is what we should perhaps consider (as opposed to object-scalar unions). |
It seems like the # These two fields are mutually exclusive:
type AreaOfTownBox {
stringValue: String
enumValue: AreaOfTown
}
# or
type IntOrStringBox {
stringValue: String
intValue: Int
} It pushes the complexity to the schema but keeps GraphQL simple and predictable. Are there downsides to that approach other than verbosity? |
@rmosolgo, you are right that it is possible but it comes at the cost of added complexity in the schema, verbosity in the queries, and having to check each value in the client side at runtime to determine which field was returned. |
Another approach inspired by @rmosolgo’s recommendation would to have a type system that looks like: union IntOrString = IntBox | StringBox
type IntBox {
value: Int
}
type StringBox {
value: String
} With a query that looks like: fragment intOrString on IntOrString {
__typename
... on IntBox { intValue: value }
... on StringBox { stringValue: value }
} |
@migueloller This problem can be solved directly in your code without any change to GraphQL.
As for IDL, I think it would be great to have support for it there, for example allowing extend to be used on enums.
@marcintustin There are number common JSON patterns that you can't express in GraphQL: key-value pairs (aka Maps), Tuples, etc.
@migueloller There are many cases when API client can't detect type based on the value returned. Also, API client should explicitly specify types which he can handle using That mean you can't serialize union of scalar types as a scalar value and forced to support some equivalents of Personally, I think unions of scalar types are frequently abused. For the cases when you return arbitrary data you can always fall-back to providing value as a JSON string. Such pattern is already used by GraphQL introspection to return Also, in SQL, you can have only one type per column and it doesn't prevent it from being dominate language for databases. |
There are legitimate use cases that are not simply cosmetic, however. Here is an example from the Facebook Marketing API. When attempting to model filtering in the API, you have an object that represents a filter expression, like so:
However,
Currently, this is not possible to express safely AFAIK. Scalar-union types would make this implementation trivial. Falling back to a JSON string for this seems like a poor solution. Some downsides:
This is a strawman. The argument does not necessitate GQL to be a superset of all protocols. Adding scalar union inputs certainly would not make GQL a superset of all protocols.
# These two fields are mutually exclusive:
type AreaOfTownBox {
stringValue: String
enumValue: AreaOfTown
}
# or
type IntOrStringBox {
stringValue: String
intValue: Int
} @rmosolgo The problem I see with this is you assume that the two fields are mutually exclusive; however, there is no way to statically define that in the current type system. |
@ianks The entire issue is about returning data, it was clearly stated in the initial comment
It has nothing to do with input objects since they are not only missing unions of scalars but don't support unions at all. If you feel that supporting unions of types (including scalars) is essential for GraphQL please open a separate issue.
In this example, you know all possible fields in advance, moreover, you know which filtering is possible on which field. So you can represent this in terms of the current GraphQL spec like below:
Advantages of this are much better autocompletion and validation on the client side than when using unions of scalars. |
@ianks here’s some related issues on union input types: #202 and graphql/graphql-js#207 |
There is one more alternative approach. You can just use custom scalars. GraphQL doesn't specify how custom scalars should be serialised so you can return anything, even free-form JSON. For example graphql-type-json and corresponding article in Apollo docs |
I was trying to do filtering by a single ID or an array of IDs and I get here :-(
now I have to go like this: In my opinion and preference, the first option is better, you always filter using id:xxx |
@jdonzet In your case, wouldn't it be better to just do:
If |
I posed a somewhat related question here: https://stackoverflow.com/q/47933512/807674 Although, my question deals with the awkwardness of modeling union variants that should contain no data other than their mere presence (i.e. singletons). I do think it would be extremely helpful to have one blessed syntactic way of approaching this data modeling question, rather than a hodgepodge of ad hoc approaches. At the end of the day, these all seem to be special cases of the question of "how far GraphQL should go from a data modeling perspective?" I would argue that obvious ways of representing algebraic data types would be extremely helpful. Mostly, this boils down to More than just a question of syntactic convenience, it means bumpy transitions for data models as they grow in complexity. |
This feature would be a big help in migrating schemas that don't use the ID type to schemas that do without introducing breaking changes. |
@benjie it seems ~1+ year later @oneOf RFC would be the place for this, yes? |
@benjie For migrating argument types: union scalar types would allow it to be done more directly as only one intermediate schema would be required to support the old and new types. My use case is migrating # with union scalar types
type Query {
post(id: Integer!): Post
}
# to
type Query {
post(id: Integer! | ID!): Post
}
# to
type Query {
post(id: ID!): Post
} # without union types
type Query {
post(id: Integer!): Post
}
# to
type Query {
post(id: Integer, new_id: ID): Post # schema no longer encompasses that an id is required
}
# to
type Query {
post(id: ID, new_id: ID): Post
}
# to
type Query {
post(id: ID!): Post # front-end has to migrate twice instead of once, very annoying
} |
With type Query {
post(by: PostSpec!): Post
}
input PostSpec @oneOf {
pk: Integer
}
# to
input PostSpec @oneOf {
pk: Integer
id: ID
}
# to
input PostSpec @oneOf {
id: ID
} I don't think there's any way to support union scalar types in general (without boxing like @OneOf does) because GraphQL cannot tell the difference between a |
Why would it need to tell the difference? Argument types are specified in the query, and there is no overloading in GraphQL |
Keep in mind custom scalars can do basically anything. Say for example you have: scalar Base64EncodedJSON When you send this to the server, you'll send something like: When the server receives it, it would then "parse" it into an internal representation, which may well be an object such as (JSON syntax): From a client perspective, From a schema point of view, there's nothing that tells GraphQL that the argument to |
I think it is acceptable to run all applicable parse methods and if multiple succeed either: throw an error, or preferably have some tiebreak like first type in the union. There is likely a more refined way of doing the tiebreaks, but I think it is far from not doable. The extra execution cost is minimal and worth it for the use case I see myself using it for. |
being on the server side, I agree with @benjie, more troubles (recognizing what the heck is this token) than benefits. A simple oneOf wrapper type would provide equivalent functionality without ambiquities |
oneOf just doesn't solve the problem sadly :( |
This sounds like ValueObjects to me so I think it's the best approach right now: union ScalarValue = IntValue | StringValue;
type IntValue{
value: Int
}
type StringValue{
value: String
} fragment scalarValue on ScalarValue {
__typename
... on IntValue {
value
}
... on StringValue {
value
}
} |
I too would enjoy the benefits of using enums in unions, it would allow me to modularise my directive inputs options which I currently have to use one big enum for 😅 |
The lack of a union scalar type exacerbates the null/undefined problem and API authors/consumers largely avoid boxing because of how unergonomic it is. A |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
Please correct me if I'm wrong at any rate, but I believe a use-case example would be the GraphQL spec's own "error type" specification in itself... According to the examples in https://spec.graphql.org/draft/#sec-Errors, the error's |
It makes sense often to have a field that could return, for example, an
Int
or aString
. It would be convenient to be able to use aresolveType
-like mechanism to determine how the value is serialized. This would be even more useful for custom scalars.The specification currently says: "GraphQL Unions represent an object that could be one of a list of GraphQL Object types", which basically means that unions are the same as interfaces, but without any shared fields. Supporting unions of scalars would further differentiate unions from interfaces.
The text was updated successfully, but these errors were encountered: