Instantly share code, notes, and snippets.

# benjie/OneOfExploration.md

Last active December 4, 2023 14:41
Show Gist options
Exploring how `oneOf` could work in a GraphQL schema

# oneOf exploration

This document is a work-in-progress exploration of how the "oneOf" solution to input polymorphism might work within a GraphQL schema.

## Base schema

For the examples below, we'll be using the following shared types using existing GraphQL syntax:

```type Person {
id: ID!
name: String!
}

type Cat {
id: ID!
name: String!
owner: Person
numberOfLives: Int
}

input CatInput {
name: String!
numberOfLives: Int
}

type Dog {
id: ID!
name: String!
owner: Person
breed: String
}

input DogInput {
name: String!
breed: String
}

"""
Some people keep cats and dogs as pets; some people keep colonies of creatures.
"""
type Colony {
id: ID!
colonyType: ColonyType
owner: Person
}

enum ColonyType {
WORM
ANT
BEE
}```

## Solution A

In solution A, the input is a single-key map from a string to the associated concrete input type (input object type, scalar, enum, etc), for example `{ "cat": { "name": "Felix", "numberOfLives": 9 } }`.

The following syntactic variants all have identical meanings; they're just different ways of expressing this concept through SDL.

(Note the scalars `integer` and `rational` have been added to demonstrate flexibility and lack of ambiguity; it's not expected that many people keep numbers as pets.)

```input PetInput @oneOf {
cat: CatInput
dog: DogInput
colony: ColonyType
integer: Int
rational: Float
}```
```inputUnion PetInput  {
cat: CatInput
dog: DogInput
colony: ColonyType
integer: Int
rational: Float
}```
```inputUnion PetInput =
| { cat: CatInput }
| { dog: DogInput }
| { colony: ColonyType }
| { integer: Int }
| { rational: Float }```
```inputUnion PetInput =
| { cat: CatInput! }
| { dog: DogInput! }
| { colony: ColonyType! }
| { integer: Int! }
| { rational: Float! }```

## Solution B

This solution is a more restricted form of Solution A, where the map key must be the exact type name of the input, for example `{ "CatInput": { "name": "Felix", "numberOfLives": 9 } }`.

This enables all the syntaxes above (although they'd look a lot more redundant), plus this further more beautiful syntax:

```inputUnion PetInput =
| CatInput
| DogInput
| ColonyType
| Int
| Float```

(This variant is slightly less flexible, in that you cannot have the same type referenced in two different ways (e.g. using String twice), but this restriction may be a benefit in terms of clarity.)

## Solution A usage

Imagine that we had the following mutation:

```extend type Mutation {
}```

In all of the Solution A cases, we could issue the mutation via this same operation document:

```mutation(\$pets: [PetInput!]!) {
id
}
}```

and for all of them, the variables would be the same:

```{
"pets": [
{ "cat": { "name": "Felix", "numberOfLives": 9 } },
{ "dog": { "name": "Buster" } },
{ "colony": "WORM" },
{ "integer": "42" },
{ "rational": "3.141592653589793" }
]
}```

## Solution B usage

Same mutation and operation document as Solution A, but the JSON would be more explicit:

```{
"pets": [
{ "CatInput": { "name": "Felix", "numberOfLives": 9 } },
{ "DogInput": { "name": "Buster" } },
{ "ColonyType": "WORM" },
{ "Int": "42" },
{ "Float": "3.141592653589793" }
]
}```

## Nesting

You could nest oneOf unions; e.g.

```inputUnion MediaInput =
| BookInput
| DVDInput

inputUnion SourceInput =
| LibraryInput
| OnlineVideoRentalInput

input BookInput {
title: String!
numberOfPages: Int
availableFrom: [SourceInput!]!
}

input DVDInput {
title: String!
durationInMinutes: Float
availableFrom: [SourceInput!]!
}

input LibraryInput {
name: String!
}

input OnlineVideoRentalInput {
name: String!
website: String!
}```

Input of a `MediaInput` might look like:

```{
"DVDInput": {
"title": "The Matrix",
"durationInMinutes": 150.3,
"availableFrom": [
{
"LibraryInput": {
"name": "Mytown Library"
}
},
{
"OnlineVideoRentalInput": {
"name": "1-line Vidz",
"website": "http://example.com/1-line"
}
}
]
}
}```

## Output oneOf - Solution A

The `oneOf` principle could also be applied to outputs; here are some equivalent syntaxes:

```type Pet @oneOf {
cat: Cat
dog: Dog
colony: ColonyType
integer: Int
rational: Float
}```

(Note: I've used the `union` keyword here, overloading `union` with two forms, but we could just as easily use a different keyword such as `oneOf`.)

```union Pet {
cat: Cat
dog: Dog
colony: ColonyType
integer: Int
rational: Float
}```
```union Pet =
| { cat: Cat }
| { dog: Dog }
| { colony: ColonyType }
| { integer: Int }
| { rational: Float }```
```union Pet =
| { cat: Cat! }
| { dog: Dog! }
| { colony: ColonyType! }
| { integer: Int! }
| { rational: Float! }```

For example, if you had the GraphQL schema:

```extend type Query {
pets: [Pet!]
}```

You could issue this query:

```{
__typename
pets {
__typename
cat {
__typename
name
numberOfLives
}
dog {
__typename
name
}
colony
integer
rational
}
}```

and you might get a response such as:

```{
"data": {
"__typename": "Query",
"pets": [
{
"__typename": "Pet",
"cat": {
"__typename": "Cat",
"name": "Felix",
"numberOfLives": 9
}
},
{
"__typename": "Pet",
"dog": {
"__typename": "Dog",
"name": "Buster"
}
},
{ "__typename": "Pet", "colony": "WORM" },
{ "__typename": "Pet", "integer": "42" },
{ "__typename": "Pet", "rational": "3.141592653589793" }
]
}
}```

💡 It might seem weird at first that `__typename` is returning `Pet` which we don't think of as a concrete type. However, it actually is a concrete type with known fields and the special semantic that exactly one non-introspection field is non-null. Effectively `Pet` is very similar to a regular object type.

This has one drawback in that if the relevant type was not queried, we do not know what it was, whereas with regular unions we could at least get its type name. This is more relevant to debugging than it is to application users.

In summary, the main differences between querying `oneOf` unions vs regular unions are:

• `oneOf` unions would not require fragments
• `oneOf` results follow the "nested" / "wrapper object" pattern
• a `__typename` directly in the selection set for the `oneOf` would return the `oneOf`'s type name rather than the concrete type (see 💡 above).

## Output oneOf - Solution B

As Solution A, but with the field name restriction. Here we'd need to use a new keyword to avoid syntax conflict with the `union` type.

```oneOf Pet =
| Cat
| Dog
| ColonyType
| Int
| Float```
```{
"data": {
"__typename": "Query",
"pets": [
{
"__typename": "Pet",
"Cat": {
"__typename": "Cat",
"name": "Felix",
"numberOfLives": 9
}
},
{
"__typename": "Pet",
"Dog": {
"__typename": "Dog",
"name": "Buster"
}
},
{ "__typename": "Pet", "Colony": "WORM" },
{ "__typename": "Pet", "Int": "42" },
{ "__typename": "Pet", "Float": "3.141592653589793" }
]
}
}```

## Arguments

oneOf could also be a way of reducing the number of "fetcher" fields required in your GraphQL schema; for example, an existing GraphQL schema might have person-finder fields such as:

```extend type Query {
personById(id: ID!): Person
personByEmail(email: String!): Person
parentOfChild(childId: ID!): Person
}```

This could be simplified to a single person fetcher field:

```input OrganizationAndDepartmentInput {
organizationId: ID!
department: Department!
}

extend type Query {
person(
id: ID
email: String
childId: ID
organizationAndDepartment: OrganizationAndDepartmentInput
): Person
}```

However, currently this loses type safety on the arguments and punts the problem of validation to the resolver.

Using a "oneOf" approach could restore this type safety, and could be applied retro-actively to schemas of the above shape and move the validation errors from the execution phase to the validation phase. The following syntaxes are equivalent:

```extend type Query {
person(
id: ID
email: String
childId: ID
organizationAndDepartment: OrganizationAndDepartmentInput
): Person @oneArgument
}```
```extend type Query {
person(
@oneOf

id: ID
email: String
childId: ID
organizationAndDepartment: OrganizationAndDepartmentInput
): Person
}```
```extend type Query {
person(
id: ID @oneOf
email: String @oneOf
childId: ID @oneOf
organizationAndDepartment: OrganizationAndDepartmentInput @oneOf
): Person
}```
```extend type Query {
person(
| { id: ID }
| { email: String }
| { childId: ID }
| { organizationAndDepartment: OrganizationAndDepartmentInput }
): Person
}```
```inputUnion PersonFinderInput =
| { id: ID }
| { email: String }
| { childId: ID }
| { organizationAndDepartment: OrganizationAndDepartmentInput }

extend type Query {
person(PersonFinderInput): Person
}```

### jchristman-plex commented Jun 16, 2020

I personally love this syntax and it would syntactically solve a search functionality I want to implement:

```input OwnerFinderInput @oneOf {
OR: [OwnerFinderInput!]
AND: [OwnerFinderInput!]
cat: CatInput
dog: DogInput
}```

This would allow for queries like:

```{
getOwner(OwnerFinderInput): [Person]
}```

The following input would find all cat owners named "bob" who also have a labrador retriever OR any owner with a dog named "fluffy"

```{
"OR": [
{
"AND": [
{
"cat": {
"owner": "bob"
}
},
{
"dog": {
}
}
]
},
{
"dog": {
"name": "fluffy"
}
}
]
}```

### benjie commented Jun 17, 2020

You can follow along with the proposal here: graphql/graphql-spec#733

I personally love this syntax and it would syntactically solve a search functionality I want to implement:

```input OwnerFinderInput @oneOf {
OR: [OwnerFinderInput!]
AND: [OwnerFinderInput!]
cat: CatInput
dog: DogInput
}```

...

@jchristman-plex You ever figure out a workaround or a way to do it differently?