https://github.com/graphql/graphql-spec/blob/master/rfcs/InputUnion.md
Last months actions:
- Write up solution 6 (merged solutions 1-3) - @leebyron
- Write up how literal values would be reflected in introspection, and contemplate uniqueness requirements - @Vince Foley
- What forms can @oneOf take? Can we express input unions in a similar way using oneOf? Can they apply to outputs? Arguments? - @Benjie
@oneOf - https://gist.github.com/benjie/e45540ad25ce9c33c2a1552da38adb91
Rough steps from previous Meeting:
- No immediate requirements for uniqueness. At a minimum, an order-based discriminant is used.
__typename
may be provided (for any input object), and this provides a discriminant.- If
__typename
is provided (for any input object) it MUST match the type name (and only that type name) expected for that input object type.- Schema validation requires all type names be unique
- If
- Introduce literal types, these may become a discriminant
inputunion SomeInput = TypeA | TypeB | TypeC
enum __TypeKind {
+ INPUT_UNION
}
Amends the "Input Object Field Names" validation rule
- Given an input object, the field
__typename
is always allowed. It must be a string. - Given an input object, if the expected input type is an input object, if provided the field
__typename
must match the name of that input type.
input Test {
field: String
}
# Legal input
{ "field": "foo" }
# Legal input
{ "field": "foo", "__typename": "Test" }
# Illegal input
{ "field": "foo", "__typename": "SomeOtherType" }
Discrimination of "ambiguous" Scalar types:
-
ex:
String
vsEnum
vsID
- Maximum one scalar (including enum) type in a union
- Otherwise, only input objects allowed (no additional unions)
- Possible Validation: a Scalar must be defined last
-
Languages have mixed capabilities for determining types from eachother
-
we want to allow possible futures that might look like
{ __typename: Int, __value: 12 }
Example of using a built-in scalar
{ "__typename": "Int", 24 }
inputunion TestUnionWScalar =
| Int
| SomeObjectType
# valid input
24
{ "some": "object" }
{ "some": "object", __typename: "SomeObjectType" }
Example of using a custom scalar
Disambiguation
inputunion TestUnionWCustomScalar =
| RawJSON
| SomeObjectType
How do new clients deal with old servers?
Clients could decide to include
__typename
in every input object.
- Clients should probably only provide
__typename
for input objects that need them - The burden to provide
__typename
when necessary likely falls on the developer rather than the framework - Clients that use introspection can look for the existence of
INPUT_UNION
in theTypeKind
enum to determine if__typename
is legal to include in an input object.
inputunion Pet = Cat | Dog
input Cat = { meowVolume: Int }
Is there a risk of runaway computational complexity if a field can be an inputunion as well?
- Yes, if there is a cycle in the input graph with an input union and other fields are not required.
- Note: implementations may want to check required fields first
inputunion Deep = Deep1 | Deep2 | String
input Deep1 { field1: Deep }
input Deep2 { field2: Deep }
{ field1: { field1: { field1: { field3: "somethingelse" } }
inputunion Many = T1 | T2 | T3 | T4
input Base { a: Int, b: Int, c: Int, field: Many }
input T1 { field: Many, a: Int }
input T2 { field: Many, b: Int }
input T3 { field: Many, c: Int }
input T4 { field: Many, d: Int }
{ field: {field: {field: {field: {field: {field: {field: {field: { error: "whoops" }}}}}}}} }
depth d union of n types O(n^d) complexity O(d) if we do field-names-only discrimination
inputunion T = T1 | T2
T1 {a: Int}
T2 {a: String}
{ a: "FOO" }
apparent options:
- complex spec/implementation to cover all these edge cases
- small spec/implementation with footguns or slowness in edge cases
- no implicit/structural discrimination; explicit only
taggedinputunion PetInput {
taggedinput PetInput {
inputunion PetInput {
cat: CatInput
dog: DogInput
colony: ColonyType
integer: Int
rational: Float
}
tagged Scalar {
str: String
int: Int
loop: Scalar
}
query ($arg: Scalar) {
foo(arg: $arg) {
field {# Scalar
__typename
}
}
}
oneof Pet {
tagged Pet {
taggedtype Pet {
taggedunion Pet {
cat: Cat
dog: Dog
colony: Colony
integer: Int
rational: Float
}
enum TypeKind {
+ INPUT_UNION # input one of unions
+ TAGGED_UNION # output one of unions
}
-
Can a tagged union type be used as a member of a traditional union?
-
Should it be a different name?
-
__typename
on the result?__typename
can always be requested on an object?- but right now
__typename
is always
-
Should the taggedUnion be "ambidexterous"?
-
Nullability / required?
- Should all fields be required to be non-null by validation rule? (Or vice versa?)
-
Arguments? Nope
-
Directives on tagged type or fields?
- Need to be added to https://spec.graphql.org/draft/#TypeSystemDirectiveLocation
- Do we allow this at all?
- No real obvious reason why not
- TAGGED_UNION and TAGGED_UNION_FIELD_DEFINITION or TAGGED_UNION_MEMBER something
type Cat {
bestBud: Cat!
}
tagged Pet @canIPutADirectiveHere {
cat: Cat @orCanIPutADirectiveHere # The name of this needs to be different for directive locations
}
type Query {
pet: Pet
}
{ pet { cat { bestBud { name }}}}
{ pet: { cat: null } }
{ pet: null }
- Should non-matching fields be included in the result?
tagged Pet { cat: Cat, dog: Dog }
query { pet: Pet }
{
pet {
cat {
name
}
dog {
name
}
}
}
# If using existing object type query rules:
{
"pet": {
"cat": { "name": "Scratchy" }
"dog": null
}
}
# New rule added: Only one field, and "exactly one field returned"?
{
"pet": {
"cat": { "name": "Scratchy" }
}
}
{
pet(name: "Whiskers") {
__taggedfield
dog {
name
}
}
}
# A: Similar to existing behavior:
# Problem: impossible to tell what type it was (since __typename is "Pet")
{
"pet": {}
}
# B: New rule?: always include the result type, but have an empty obj for no fields queried?
{
"pet": {"cat": {}}
}
# C: New rule? always include the result type, but have it be null
# Problem: implies the cat was null! DEFINITELY WRONG!
{
"pet": {"cat": null}
}
# D: Raises an error if not requested
# Problem: Backwards breaking
{
"pet": null # with error
}
# Existing union case:
{
pet(name: "Whiskers") {
__typename
... on Dog {
name
}
}
}
{
"pet": { "__typename": "Cat" }
}
- When should you use tagged vs union for output?
- Tagged:
- Can't query across all types that implement an interface
- Can't get the
__typename
of the resulting type - Can get the structure of the result without having to use
__typename
- Tagged:
ACTION - @benjie: rename all "OneOf" / "OneField" to "tagged union" in InputUnions RFC doc.
What does tagged give us?
- dealing with boxed scalar input and output
- _lt and _gt are both Int
- symmetry between input and output
What doesn't tagged output give us that union does?
- __typename of unknown type
- spreading unions
I can't wait for this feature! GQL unions feel really incomplete without it.