diff --git a/cspell.yml b/cspell.yml index 0ea1def96..1a7446cbe 100644 --- a/cspell.yml +++ b/cspell.yml @@ -21,6 +21,8 @@ words: - tatooine - zuck - zuckerberg + - brontie + - oneOf # Forbid Alternative spellings flagWords: - implementor diff --git a/spec/Section 3 -- Type System.md b/spec/Section 3 -- Type System.md index 8400233fa..ea15d6b33 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1588,6 +1588,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** + +:: A _OneOf Input Object_ is a special variant of Input Object 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 fields be provided): + +```graphql +input UserUniqueCondition @oneOf { + id: ID + username: String + organizationAndEmail: OrganizationAndEmailInput +} +``` + +In schema introspection, the `__Type.isOneOf` 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 @@ -1682,6 +1704,17 @@ is constructed with the following rules: variable definition 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: + +- Prior to construction of the coerced map via the rules above: the value to be + coerced must contain exactly one entry and that entry must not be {null} or + the {null} literal, otherwise a _request error_ must be raised. + +- The map resulting from the input coercion rules above must contain exactly one + entry and the value for that entry must not be {null}, otherwise an _execution + error_ must be raised. + Following are examples of input coercion for an input object type with a `String` field `a` and a required (non-null) `Int!` field `b`: @@ -1711,6 +1744,46 @@ 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 ExampleOneOfInputObject @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 | +| `{ a: null, b: null }` | `{}` | Error: Exactly one key must be specified | +| `{ a: null }` | `{}` | Error: Value for member field {a} must be non-null | +| `{ b: 123 }` | `{}` | `{ b: 123 }` | +| `{}` | `{}` | Error: Exactly one key must be specified | +| `{ a: $a, b: 123 }` | `{ a: null }` | Error: Exactly one key must be specified | +| `{ a: $a, b: 123 }` | `{}` | Error: Exactly one key must be specified | +| `{ a: $a, b: $b }` | `{ a: "abc" }` | Error: Exactly one key must be specified | +| `{ b: $b }` | `{ b: 123 }` | `{ b: 123 }` | +| `$var` | `{ var: { b: 123 } }` | `{ b: 123 }` | +| `$var` | `{ var: { a: "abc", b: 123 } }` | Error: Exactly one key must be specified | +| `$var` | `{ var: { a: "abc", b: null } }` | Error: Exactly one key must be specified | +| `$var` | `{ var: { a: null } }` | Error: Value for member field {a} must be non-null | +| `$var` | `{ var: {} }` | Error: Exactly one key must be specified | +| `"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} | +| `$var` | `{ var: { b: "abc" } }` | Error: Incorrect value for member field {b} | +| `{ a: "abc" }` | `{}` | `{ a: "abc" }` | +| `{ b: $b }` | `{}` | Error: Value for member field {b} must be specified | +| `$var` | `{ var: { a: "abc" } }` | `{ a: "abc" }` | +| `{ a: "abc", b: null }` | `{}` | Error: Exactly one key must be specified | +| `{ b: $b }` | `{ b: null }` | Error: Value for member field {b} must be non-null | +| `{ b: 123, c: "xyz" }` | `{}` | Error: Unexpected field {c} | +| `$var` | `{ var: { b: 123, c: "xyz" } }` | Error: Unexpected field {c} | + **Type Validation** 1. An Input Object type must define one or more input fields. @@ -1723,6 +1796,9 @@ input ExampleInputObject { returns {true}. 4. If input field type is Non-Null and a default value is not defined: 1. The `@deprecated` directive must not be applied to this input field. + 5. 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. @@ -1788,6 +1864,12 @@ defined. the previous Input Object. 4. Any non-repeatable directives provided must not already apply to the previous Input Object type. +5. The `@oneOf` directive must not be provided by an Input Object type + extension. +6. 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 @@ -2013,6 +2095,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. @@ -2227,3 +2312,20 @@ 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` _built-in directive_ is used within the type system definition +language to indicate an Input Object is a OneOf Input Object. + +```graphql example +input UserUniqueCondition @oneOf { + id: ID + username: String + organizationAndEmail: OrganizationAndEmailInput +} +``` diff --git a/spec/Section 4 -- Introspection.md b/spec/Section 4 -- Introspection.md index d7f8e629f..7643017de 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -151,6 +151,8 @@ type __Type { inputFields(includeDeprecated: Boolean! = false): [__InputValue!] # must be non-null for NON_NULL and LIST, otherwise null. ofType: __Type + # must be non-null for INPUT_OBJECT, otherwise null. + isOneOf: Boolean } enum __TypeKind { @@ -373,6 +375,8 @@ Fields\: - `inputFields` must return the set of input fields as a list of `__InputValue`. - Accepts the argument `includeDeprecated` which defaults to {false}. If {true}, deprecated input fields are also returned. +- `isOneOf` must return {true} when representing a _OneOf Input Object_, + otherwise {false}. - All other fields must return {null}. **List** diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 88f1e4048..488881f88 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -40,6 +40,11 @@ type Query { findDog(searchBy: FindDogInput): Dog } +type Mutation { + addPet(pet: PetInput!): Pet + addPets(pets: [PetInput!]!): [Pet] +} + enum DogCommand { SIT DOWN @@ -92,6 +97,23 @@ input FindDogInput { name: String owner: String } + +input CatInput { + name: String! + nickname: String + meowVolume: Int +} + +input DogInput { + name: String! + nickname: String + barkVolume: Int +} + +input PetInput @oneOf { + cat: CatInput + dog: DogInput +} ``` ## Documents @@ -1462,6 +1484,12 @@ query goodComplexDefaultValue($search: FindDogInput = { name: "Fido" }) { name } } + +mutation addPet($pet: PetInput! = { cat: { name: "Brontie" } }) { + addPet(pet: $pet) { + name + } +} ``` Non-coercible values (such as a String into an Int) are invalid. The following @@ -1477,6 +1505,24 @@ query badComplexValue { name } } + +mutation oneOfWithNoFields { + addPet(pet: {}) { + name + } +} + +mutation oneOfWithTwoFields($dog: DogInput) { + addPet(pet: { cat: { name: "Brontie" }, dog: $dog }) { + name + } +} + +mutation listOfOneOfWithNullableVariable($dog: DogInput) { + addPets(pets: [{ dog: $dog }]) { + name + } +} ``` ### Input Object Field Names @@ -2003,8 +2049,8 @@ IsVariableUsageAllowed(variableDefinition, variableUsage): - Let {variableType} be the expected type of {variableDefinition}. - Let {locationType} be the expected type of the {Argument}, {ObjectField}, or {ListValue} entry where {variableUsage} is located. -- If {locationType} is a non-null type AND {variableType} is NOT a non-null - type: +- If {IsNonNullPosition(locationType, variableUsage)} AND {variableType} is NOT + a non-null type: - Let {hasNonNullVariableDefaultValue} be {true} if a default value exists for {variableDefinition} and is not the value {null}. - Let {hasLocationDefaultValue} be {true} if a default value exists for the @@ -2015,6 +2061,15 @@ IsVariableUsageAllowed(variableDefinition, variableUsage): - Return {AreTypesCompatible(variableType, nullableLocationType)}. - Return {AreTypesCompatible(variableType, locationType)}. +IsNonNullPosition(locationType, variableUsage): + +- If {locationType} is a non-null type, return {true}. +- If the location of {variableUsage} is an {ObjectField}: + - Let {parentObjectValue} be the {ObjectValue} containing {ObjectField}. + - Let {parentLocationType} be the expected type of {ObjectValue}. + - If {parentLocationType} is a _OneOf Input Object_ type, return {true}. +- Return {false}. + AreTypesCompatible(variableType, locationType): - If {locationType} is a non-null type: @@ -2103,6 +2158,30 @@ query listToNonNullList($booleanList: [Boolean]) { This would fail validation because a `[T]` cannot be passed to a `[T]!`. Similarly a `[T]` cannot be passed to a `[T!]`. +Variables used for OneOf Input Object fields must be non-nullable. + +```graphql example +mutation addCat($cat: CatInput!) { + addPet(pet: { cat: $cat }) { + name + } +} + +mutation addCatWithDefault($cat: CatInput! = { name: "Brontie" }) { + addPet(pet: { cat: $cat }) { + name + } +} +``` + +```graphql counter-example +mutation addNullableCat($cat: CatInput) { + addPet(pet: { cat: $cat }) { + name + } +} +``` + **Allowing Optional Variables When Default Values Exist** A notable exception to typical variable type compatibility is allowing a