Skip to content

Instantly share code, notes, and snippets.

@leebyron
Created May 28, 2020 18:54
Show Gist options
  • Save leebyron/f7f9d81c7ca5259357fab5d82a4c0621 to your computer and use it in GitHub Desktop.
Save leebyron/f7f9d81c7ca5259357fab5d82a4c0621 to your computer and use it in GitHub Desktop.
Input Union Meeting 2020/5/28

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

Solution 6:

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
  • Introduce literal types, these may become a discriminant

Type definition language

inputunion SomeInput = TypeA | TypeB | TypeC

Introspection addition

enum __TypeKind {
  + INPUT_UNION
}

Input object type restriction

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 vs Enum vs ID

    • 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

Forward compatibility

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 the TypeKind enum to determine if __typename is legal to include in an input object.
inputunion Pet = Cat | Dog
input Cat = { meowVolume: Int }

Nested input unions?

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

"One of" grammatical approach

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
}

Open questions:

  • 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?

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

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
@vrheaume-ikarus
Copy link

I can't wait for this feature! GQL unions feel really incomplete without it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment