Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: OneOf Input Objects #825

Open
wants to merge 36 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
c385058
Renumber list items
benjie Feb 19, 2021
f6bd659
@oneOf input objects
benjie Feb 19, 2021
39e593c
@oneOf fields
benjie Feb 19, 2021
b6741c3
Fix typos (thanks @eapache!)
benjie Feb 26, 2021
d17d5ec
Much stricter validation for oneof literals (with examples)
benjie Mar 6, 2021
dca3826
Add missing coercion rule
benjie Mar 6, 2021
7e02f5a
Clearer wording of oneof coercion rule
benjie Mar 6, 2021
4111476
Add more examples for clarity
benjie Mar 6, 2021
6754e0a
Rename introspection fields to oneOf
benjie Mar 6, 2021
7c4c1a2
Oneof's now require exactly one field/argument, and non-nullable vari…
benjie Mar 6, 2021
bb225f7
Remove extraneous newline
benjie Mar 6, 2021
05fde06
graphgl -> graphql
benjie Apr 8, 2021
e8f6145
Apply suggestions from @eapache's review
benjie Apr 8, 2021
08abf49
Apply suggestions from code review
benjie Dec 23, 2021
59cb12d
Update spec/Section 3 -- Type System.md
benjie Jan 4, 2022
c470afb
Merge branch 'main' into oneof-v2
benjie Mar 22, 2022
99aa5d9
Remove Oneof Fields from spec
benjie Mar 22, 2022
691087d
Oneof -> OneOf
benjie Mar 22, 2022
7109dbc
Spellings
benjie Mar 22, 2022
05ab541
Remove out of date example
benjie May 6, 2022
6a6be52
Rename __Type.oneOf to __Type.isOneOf
benjie May 25, 2022
de87d2f
Add a:null example
benjie May 25, 2022
57e2388
Rewrite to avoid ambiguity of language
benjie May 25, 2022
5a966f2
Forbid 'extend input' from introducing the @oneOf directive
benjie May 26, 2022
e78d2b5
Merge branch 'main' into oneof-v2
benjie Nov 13, 2023
c6cd857
Merge branch 'main' into oneof-v2
benjie Mar 27, 2024
d106233
Add yet more examples to the example coercion table
benjie Mar 27, 2024
87d0b22
Indicate `@oneOf` is a built-in directive
benjie Jun 4, 2024
d88d62a
Update spec/Section 3 -- Type System.md
benjie Jun 5, 2024
a810aef
Merge branch 'main' into oneof-v2
benjie Jul 19, 2024
a1563a9
remove OneOf-specific rule in favor of update to VariablesInAllowedPo…
yaacovCR Sep 21, 2024
b45c0e4
Clarify IsNonNullPosition algorithm
benjie Oct 17, 2024
0c9830e
Clarify OneOf examples
benjie Oct 17, 2024
c4d0b50
Add more examples
benjie Oct 17, 2024
340594e
Remove new validation rule in favour of updates to existing rules
benjie Oct 17, 2024
dbccf84
Null literal is separate
benjie Oct 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cspell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,5 @@ words:
- tatooine
- zuck
- zuckerberg
- brontie
- oneOf
106 changes: 106 additions & 0 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -1520,6 +1520,28 @@ define arguments or contain references to interfaces and unions, neither of
which is appropriate for use as an input argument. For this reason, input
objects have a separate type in the system.

**OneOf Input Objects**

OneOf Input Objects are a special variant of Input Objects where the type system
asserts that exactly one of the fields must be set and non-null, all others
being omitted. This is useful for representing situations where an input may be
one of many different options.

When using the type system definition language, the `@oneOf` directive is used
to indicate that an Input Object is a OneOf Input Object (and thus requires
exactly one of its field be provided):

```graphql
input UserUniqueCondition @oneOf {
id: ID
username: String
organizationAndEmail: OrganizationAndEmailInput
}
```

In schema introspection, the `__Type.oneOf` field will return {true} for OneOf
Input Objects, and {false} for all other Input Objects.

**Circular References**

Input Objects are allowed to reference other Input Objects as field types. A
Expand Down Expand Up @@ -1613,6 +1635,21 @@ is constructed with the following rules:
does not provide a default value, the input object field definition's default
value should be used.

Further, if the input object is a OneOf Input Object, the following additional
rules apply:

- If the input object literal or unordered map does not contain exactly one
entry, an error must be thrown.

- If the single entry in the input object literal or unordered map is {null}, an
error must be thrown.
benjie marked this conversation as resolved.
Show resolved Hide resolved

- If the coerced unordered map does not contain exactly one entry, an error must
be thrown.

- If the value of the single entry in the coerced unordered map is {null}, an
error must be thrown.

Following are examples of input coercion for an input object type with a
`String` field `a` and a required (non-null) `Int!` field `b`:

Expand Down Expand Up @@ -1642,6 +1679,37 @@ input ExampleInputObject {
| `{ b: $var }` | `{ var: null }` | Error: {b} must be non-null. |
| `{ b: 123, c: "xyz" }` | `{}` | Error: Unexpected field {c} |

Following are examples of input coercion for a oneOf input object type with a
`String` member field `a` and an `Int` member field `b`:

```graphql example
input ExampleInputTagged @oneOf {
a: String
b: Int
}
```

| Literal Value | Variables | Coerced Value |
| ------------------------ | ----------------------- | --------------------------------------------------- |
| `{ a: "abc", b: 123 }` | `{}` | Error: Exactly one key must be specified |
| `{ a: null, b: 123 }` | `{}` | Error: Exactly one key must be specified |
| `{ b: 123 }` | `{}` | `{ b: 123 }` |
benjie marked this conversation as resolved.
Show resolved Hide resolved
| `{ a: $var, b: 123 }` | `{ var: null }` | Error: Exactly one key must be specified |
| `{ a: $var, b: 123 }` | `{}` | Error: Exactly one key must be specified |
| `{ b: $var }` | `{ var: 123 }` | `{ b: 123 }` |
| `$var` | `{ var: { b: 123 } }` | `{ b: 123 }` |
| `"abc123"` | `{}` | Error: Incorrect value |
| `$var` | `{ var: "abc123" } }` | Error: Incorrect value |
| `{ a: "abc", b: "123" }` | `{}` | Error: Exactly one key must be specified |
| `{ b: "123" }` | `{}` | Error: Incorrect value for member field {b} |
| `{ a: "abc" }` | `{}` | `{ a: "abc" }` |
| `{ b: $var }` | `{}` | Error: Value for member field {b} must be specified |
| `$var` | `{ var: { a: "abc" } }` | `{ a: "abc" }` |
benjie marked this conversation as resolved.
Show resolved Hide resolved
| `{ a: "abc", b: null }` | `{}` | Error: Exactly one key must be specified |
| `{ b: $var }` | `{ var: null }` | Error: Value for member field {b} must be non-null |
| `{ b: 123, c: "xyz" }` | `{}` | Error: Unexpected field {c} |
| `{ a: $a, b: $b }` | `{ a: "abc" }` | Error: Exactly one key must be specified |
benjie marked this conversation as resolved.
Show resolved Hide resolved

benjie marked this conversation as resolved.
Show resolved Hide resolved
**Type Validation**

1. An Input Object type must define one or more input fields.
michaelstaib marked this conversation as resolved.
Show resolved Hide resolved
Expand All @@ -1652,6 +1720,9 @@ input ExampleInputObject {
{"\_\_"} (two underscores).
3. The input field must accept a type where {IsInputType(inputFieldType)}
returns {true}.
4. If the Input Object is a OneOf Input Object then:
1. The type of the input field must be nullable.
2. The input field must not have a default value.
3. If an Input Object references itself either directly or through referenced
Input Objects, at least one of the fields in the chain of references must be
either a nullable or a List type.
Expand Down Expand Up @@ -1679,6 +1750,10 @@ defined.
the original Input Object.
4. Any non-repeatable directives provided must not already apply to the original
Input Object type.
5. If the original Input Object is a OneOf Input Object then:
1. All fields of the Input Object type extension must be nullable.
2. All fields of the Input Object type extension must not have default
values.

## List

Expand Down Expand Up @@ -1898,6 +1973,9 @@ schema.
GraphQL implementations that support the type system definition language should
provide the `@specifiedBy` directive if representing custom scalar definitions.

GraphQL implementations that support the type system definition language should
provide the `@oneOf` directive if representing OneOf Input Objects.

When representing a GraphQL schema using the type system definition language any
_built-in directive_ may be omitted for brevity.

Expand Down Expand Up @@ -2085,3 +2163,31 @@ to the relevant IETF specification.
```graphql example
scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122")
```

### @oneOf

```graphql
directive @oneOf on INPUT_OBJECT
```

The `@oneOf` directive is used within the type system definition language to
indicate an Input Object is a OneOf Input Object.
benjie marked this conversation as resolved.
Show resolved Hide resolved

```graphql example
input UserUniqueCondition @oneOf {
id: ID
username: String
organizationAndEmail: OrganizationAndEmailInput
}
```

```graphql example
type Query {
benjie marked this conversation as resolved.
Show resolved Hide resolved
findUser(
byID: ID
byUsername: String
byEmail: String
byRegistrationNumber: Int
): User @oneOf
}
```
4 changes: 4 additions & 0 deletions spec/Section 4 -- Introspection.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ type __Type {
ofType: __Type
# may be non-null for custom SCALAR, otherwise null.
specifiedByURL: String
# should be non-null for INPUT_OBJECT only
benjie marked this conversation as resolved.
Show resolved Hide resolved
oneOf: Boolean
}

enum __TypeKind {
Expand Down Expand Up @@ -367,6 +369,8 @@ Fields\:
- `name` must return a String.
- `description` may return a String or {null}.
- `inputFields` must return the set of input fields as a list of `__InputValue`.
- `oneOf` must return {true} when representing a OneOf Input Object, {false}
otherwise.
- All other fields must return {null}.

**List**
Expand Down
118 changes: 118 additions & 0 deletions spec/Section 5 -- Validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ type Query {
dog: Dog
}

type Mutation {
addPet(pet: PetInput): Pet
}

enum DogCommand {
SIT
DOWN
Expand Down Expand Up @@ -87,6 +91,23 @@ type Cat implements Pet {
union CatOrDog = Cat | Dog
union DogOrHuman = Dog | Human
union HumanOrAlien = Human | Alien

input CatInput {
name: String!
nickname: String
meowVolume: Int
}

input DogInput {
name: String!
nickname: String
barkVolume: Int
}

input PetInput @oneOf {
cat: CatInput
dog: DogInput
}
```

## Documents
Expand Down Expand Up @@ -1406,6 +1427,103 @@ arguments, an input object may have required fields. An input field is required
if it has a non-null type and does not have a default value. Otherwise, the
input object field is optional.

### OneOf Input Objects Have Exactly One Field

**Formal Specification**

- For each {operation} in {document}:
- Let {oneofInputObjects} be all OneOf Input Objects transitively included in
the {operation}.
- For each {oneofInputObject} in {oneofInputObjects}:
- Let {fields} be the fields provided by {oneofInputObject}.
benjie marked this conversation as resolved.
Show resolved Hide resolved
- {fields} must contain exactly one entry.
- Let {field} be the sole entry in {fields}.
- Let {value} be the value of {field}.
- {value} must not be the {null} literal.
- If {value} is a variable:
- Let {variableName} be the name of {variable}.
- Let {variableDefinition} be the {VariableDefinition} named
{variableName} defined within {operation}.
- Let {variableType} be the expected type of {variableDefinition}.
- {variableType} must be a non-null type.
Copy link
Contributor

@yaacovCR yaacovCR Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if these lines relating to variable usage pre-empt the discussion around #1059 and should be pulled from this spec change (simplifying it).

Variables must be only of the allowed type, but it seems that we should specify what that entails for all variables and types only in one place, i.e. the separate rule.

So if we currently allow variables of nullable types to be used in non-null positions and throw a field error at runtime -- which we do -we should continue to do so irrespective of isOneOf, and if/when we make the change there, that should be done in a way that covers isOneOf as well.

Encountered this while attempting to rebase graphql/graphql-js#3813

Copy link
Contributor

@yaacovCR yaacovCR Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two arguments against this:

  1. The benefit of the current relaxed version of VariablesInAllowedPositions is that you can use a variable for an argument, not supply the value, and get the default. But with OneOf, default values are not allowed for any fields, so treating this as the same as the general case would only be sensical if (a) we adopt the strict version of the general rule (b) we can convince ourselves that there is a real value in consistency almost for consistency's case.
  2. We can only consider this to be a specific case of the general rule if the @oneof directive is held to transform all of the input object's field types into non-nullable (but still not required) types. Then, these become non-nullable positions. There is a certain ambiguity as to whether the field types themselves are nullable or not. By syntax, we want to make sure older clients can leave them out, and so we define them to be nullable. But for clients aware of @oneof, presumably we are ok to define them as non-nullable, with the caveat that there would have to be a change to the IsRequiredInputField algorithm. Currently, an input object field is required if is of a non-null type and does not have a default value. This would have to be changed to have an additional condition, that the parent input object does not have isOneOf to be true. Note that IsRequiredInputField is part of graphql-js as a utility, and referenced many times in the spec, but does not form a formal agorithm.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if these lines relating to variable usage pre-empt the discussion around #1059 and should be pulled from this spec change (simplifying it).

I don't think so? Technically all fields on a oneof are nullable, but you must specify one and it must be non-null, so this seems a very straightforward way to require that when it comes to a variable? #1059 handles non-null positions, but this is a nullable position according to the type system.

Copy link
Contributor

@yaacovCR yaacovCR Mar 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and it must be non-null

So it's a nullable type only because we want introspection to say that it's nullable because currently that's the only way of making something optional. But a null cannot be supplied, so in a sense it's by definition "a non-nullable position."

So we would then have to introduce the concept of non-nullable positions that occur when (1) the type for the position is non-nullable or (2) the containing type is oneOf, and then the general rule about matching nullable and non-nullable would have to depend on this new "position" concept rather than the type itself.

As I type this, I can see that this additional layer is a bad idea, and I appreciate the compromise that you have ended up with.

On the other hand, in GraphQL 2.0 / TreeQL, we should definitely separate optionality and nullability, and remember to change oneOf to be better defined. (It really shouldn't be the case that you cannot use null at a nullable position.)


**Explanatory Text**

OneOf Input Objects require that exactly one field must be supplied and that
field must not be {null}.

An empty OneOf Input Object is invalid.

```graphql counter-example
mutation addPet {
addPet(pet: {}) {
name
}
}
```

Multiple fields are not allowed.

```graphql counter-example
mutation addPet($cat: CatInput, $dog: DogInput) {
addPet(pet: { cat: $cat, dog: $dog }) {
name
}
}
```

```graphql counter-example
mutation addPet($dog: DogInput) {
addPet(pet: { cat: { name: "Brontie" }, dog: $dog }) {
name
}
}
```

```graphql counter-example
mutation addPet {
addPet(pet: { cat: { name: "Brontie" }, dog: null }) {
name
}
}
```

Variables used for OneOf Input Object fields must be non-nullable.

```graphql example
mutation addPet($cat: CatInput!) {
addPet(pet: { cat: $cat }) {
name
}
}
```

```graphql counter-example
mutation addPet($cat: CatInput) {
addPet(pet: { cat: $cat }) {
name
}
}
```

If a field with a literal value is present then the value must not be {null}.

```graphql example
mutation addPet {
addPet(pet: { cat: { name: "Brontie" } }) {
name
}
}
```

```graphql counter-example
mutation addPet {
addPet(pet: { cat: null }) {
name
}
}
```

## Directives

### Directives Are Defined
Expand Down