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 theoneof
field{oneof cat:Cat, dog:Dog }
- looks like theoneof
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
}