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..35f7e9bd0 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1568,7 +1568,7 @@ InputFieldsDefinition : { InputValueDefinition+ } Fields may accept arguments to configure their behavior. These inputs are often scalars or enums, but they sometimes need to represent more complex values. -A GraphQL Input Object defines a set of input fields; the input fields are +:: A GraphQL _Input Object_ defines a set of input fields; the input fields are scalars, enums, other input objects, or any wrapping type whose underlying base type is one of those three. This allows arguments to accept arbitrarily complex structs. @@ -1723,6 +1723,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. @@ -1765,6 +1768,72 @@ InputFieldDefaultValueHasCycle(field, defaultValue, visitedFields): - Return {InputObjectDefaultValueHasCycle(namedFieldType, fieldDefaultValue, nextVisitedFields)}. +### OneOf Input Objects + +:: A _OneOf Input Object_ is a special variant of _Input Object_ where exactly +one field 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`](#sec--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. + +**Input Coercion** + +The value of a OneOf Input Object, as a variant of Input Object, must also be an +input object literal or an unordered map supplied by a variable, otherwise a +_request error_ must be raised. + +- Prior to construction of the coerced map via the input coercion rules of an + _Input Object_: 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. + +- All _Input Object_ + [input coercion rules](http://localhost:3000/draft#sec-Input-Objects.Input-Coercion) + must also apply to an _OneOf Input Object_. + +- The resulting coerced map must contain exactly one entry and the value for + that entry must not be {null}, otherwise an _execution error_ must be raised. + +Following are additional 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" }` | `{}` | `{ a: "abc" }` | +| `{ b: 123 }` | `{}` | `{ b: 123 }` | +| `$var` | `{ var: { a: "abc" } }` | `{ a: "abc" }` | +| `{ a: null }` | `{}` | Error: Value for member field {a} must be non-null | +| `$var` | `{ var: { a: null } }` | Error: Value for member field {a} must be non-null | +| `{ a: $a }` | `{}` | Error: Value for member field {a} must be specified | +| `{ a: "abc", b: 123 }` | `{}` | Error: Exactly one key must be specified | +| `{ a: 456, b: "xyz" }` | `{}` | Error: Exactly one key must be specified | +| `$var` | `{ var: { a: "abc", b: 123 } }` | Error: Exactly one key must be specified | +| `{ a: "abc", b: null }` | `{}` | Error: Exactly one key must be specified | +| `{ a: "abc", b: $b }` | `{}` | Error: Exactly one key must be specified | +| `{ a: $a, b: $b }` | `{ a: "abc" }` | Error: Exactly one key must be specified | +| `{}` | `{}` | Error: Exactly one key must be specified | +| `$var` | `{ var: {} }` | Error: Exactly one key must be specified | + ### Input Object Extensions InputObjectTypeExtension : @@ -1788,6 +1857,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 +2088,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 +2305,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