Skip to content

Instantly share code, notes, and snippets.

@benjie
Last active Jan 23, 2021
Embed
What would you like to do?
Potential ideas for syntax to use for the "oneof" input/argument/output polymorphism proposal to the GraphQL Spec.

oneof syntax discussion

After going full circle on @oneField directive -> tagged type -> back again, the GraphQL Input Unions Working Group have determined that our current best proposal is to add a variant of the existing input object type that accepts only one field, and a variant of object fields that accept only one argument. Though the changes to introspection for this are likely to be relatively small (e.g. adding something like __Type.isOneOf: Boolean and __Field.isOneOf: Boolean to the introspection schema), how to express this in SDL/IDL is much less clear.

At this stage in the proposal the actual syntax we'll land on isn't super important (we're still seeing if it actually solves the problems, and if it does so with acceptable trade-offs), but nonetheless it warrants some exploration, so I've outlined some alternative syntax proposals for the oneof solution.

IMPORTANT: every single one of these is identical in terms of functionality and introspection, the only difference is in schema language expression of the feature. The difference is only visual, not functional.

Relevant: previous gist on syntaxes proposed for the @oneField/@oneOf proposal

NOTE: the old @oneField RFC is out of date, and a new one is still being drafted, so there's nothing to see on this front as of today (23rd January 2020).

Keyword before parenthesis:

Where parenthesis is { or ( as appropriate.

Pro: consistent across fields and arguments.

Con: field syntax is a bit weird (looks less like a function definition than usual).

CONCLUSION: shortlist.

# Input object type
input PetInput oneof {
  """
  A cat
  """
  cat: CatInput

  """
  A dog
  """
  dog: DogInput

  fish: FishInput
}

# Object type
type Pet oneof {
  """
  A cat
  """
  cat: Cat

  """
  A dog
  """
  dog: Dog

  fish: Fish
}

type Query {
  # For arguments
  user oneof (
    """
    By their globally unique identifier
    """
    byId: ID

    """
    By their unique username
    """
    byUserName: String

    byEmail: String
  ): User
}

Keyword immediately inside parenthesis:

Where parenthesis is { or ( as appropriate.

Pro: consistent across fields and arguments.

Con: with commas being ignored in GraphQL the following would all be valid:

  • {oneof, cat:Cat, dog:Dog} - looks like a mistake, like you forgot to add a type for the oneof field
  • {oneof cat:Cat, dog:Dog } - looks like the oneof only applies to the first field
  • {oneof cat:Cat dog:Dog} - looks like both of the problems described above combined

Con: would make a field name oneof which was previously valid (and remains valid) look really ambiguous: {oneof: Pet} vs {oneof oneof: Pet manyof: Herd}.

Con: U.G.L.Y, you ain't got no alibi.

CONCLUSION: avoid.

# Input object type
input PetInput {
  oneof

    """
    A cat
    """
    cat: CatInput

    """
    A dog
    """
    dog: DogInput

    fish: FishInput
}

# Object type
type Pet {
  oneof

    """
    A cat
    """
    cat: Cat

    """
    A dog
    """
    dog: Dog

    fish: Fish
}

type Query {
  # For arguments
  user (
    oneof

      """
      By their globally unique identifier
      """
      byId: ID

      """
      By their unique username
      """
      byUserName: String

      byEmail: String
  ): User
}

Alternative syntax

NOTE: unlike with unions, we'd need the leading pipe to differentiate between a oneof or regular entity that only has one field/argument.

NOTE 2: we could use ^ or any other symbol instead of |.

Pro: consistent across fields and arguments.

Pro: even for a long fields/args list, you know each arg is a oneof without having to look at the type/field definition.

Con: potentially verbose.

Con: putting documentation before the pipe looks weird. Putting it after definitely feels wrong though.

Con: feels like the type definition itself should state it's a oneof rather than the arguments/fields.

CONCLUSION: shortlist.

# Input object type
input PetInput {
  """
  A cat
  """
  | cat: CatInput

  """
  A dog
  """
  | dog: DogInput

  | fish: FishInput
}

# Object type
type Pet {
  """
  A cat
  """
  | cat: Cat

  """
  A dog
  """
  | dog: Dog

  | fish: Fish
}

type Query {
  # For arguments
  user (
    """
    By their globally unique identifier
    """
    | byId: ID

    """
    By their unique username
    """
    | byUserName: String

    | byEmail: String
  ): User
}

Directive:

Pro: no syntax changes required to GraphQL spec.

Pro: existing GraphQL tools can read the SDL, and even if they don't understand the @oneOf directive they can still produce useful output.

Con: particularly far removed when defining arguments lists; feels wrong here.

CONCLUSION: shortlist.

# Input object type
input PetInput @oneOf {
  """
  A cat
  """
  cat: CatInput

  """
  A dog
  """
  dog: DogInput

  fish: FishInput
}

# Object type
type Pet @oneOf {
  """
  A cat
  """
  cat: Cat

  """
  A dog
  """
  dog: Dog

  fish: Fish
}

type Query {
  # For arguments
  user (
    """
    By their globally unique identifier
    """
    byId: ID

    """
    By their unique username
    """
    byUserName: String

    byEmail: String
  ): User @oneOf
}

Prefix keyword:

Pro: consistent across fields and arguments.

Pro: makes it clear that this is a modifier to existing behaviour (like the async in async function foo() {} in ES2017, for example).

Pro: looks nicer for field arguments (they still appear like a function definition).

Con: feels like it applies to the type/field rather than its fields/arguments; potentially not so intuitive?

CONCLUSION: shortlist.

# Input object type
oneof input PetInput {
  """
  A cat
  """
  cat: CatInput

  """
  A dog
  """
  dog: DogInput

  fish: FishInput
}

# Object type
oneof type Pet {
  """
  A cat
  """
  cat: Cat

  """
  A dog
  """
  dog: Dog

  fish: Fish
}

type Query {
  # For arguments
  oneof user(
    """
    By their globally unique identifier
    """
    byId: ID

    """
    By their unique username
    """
    byUserName: String

    byEmail: String
  ): User
}

Replacement keyword:

Inconsistent between types and fields.

CONCLUSION: avoid.

# Input object type
oneofInput PetInput {
  """
  A cat
  """
  cat: CatInput

  """
  A dog
  """
  dog: DogInput

  fish: FishInput
}

# Object type
oneofType Pet {
  """
  A cat
  """
  cat: Cat

  """
  A dog
  """
  dog: Dog

  fish: Fish
}

type Query {
  # For arguments
  oneof user(
    """
    By their globally unique identifier
    """
    byId: ID

    """
    By their unique username
    """
    byUserName: String

    byEmail: String
  ): User
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment