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 4f9356f0c..3dfe9b0be 100644 --- a/spec/Section 3 -- Type System.md +++ b/spec/Section 3 -- Type System.md @@ -1566,6 +1566,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.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 @@ -1659,6 +1681,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. + +- Within the input object literal or unordered map, if the single entry is + {null} or the {null} literal an error must be thrown. + +- 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`: @@ -1688,6 +1725,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 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 | +| `{ 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. @@ -1700,6 +1777,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. @@ -1727,6 +1807,12 @@ defined. the original Input Object. 4. Any non-repeatable directives provided must not already apply to the original 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 @@ -1949,6 +2035,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. @@ -2162,3 +2251,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 d2ec33b6b..c02f9eb21 100644 --- a/spec/Section 4 -- Introspection.md +++ b/spec/Section 4 -- Introspection.md @@ -151,6 +151,8 @@ type __Type { ofType: __Type # may be non-null for custom SCALAR, otherwise null. specifiedByURL: String + # should be non-null for INPUT_OBJECT only + 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, {false} + otherwise. - All other fields must return {null}. **List** diff --git a/spec/Section 5 -- Validation.md b/spec/Section 5 -- Validation.md index 75af96ffd..bf1a44b4d 100644 --- a/spec/Section 5 -- Validation.md +++ b/spec/Section 5 -- Validation.md @@ -41,6 +41,11 @@ type Query { findDog(searchBy: FindDogInput): Dog } +type Mutation { + addPet(pet: PetInput!): Pet + addPets(pet: [PetInput!]!): [Pet] +} + enum DogCommand { SIT DOWN @@ -93,6 +98,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 @@ -1334,6 +1356,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 @@ -1349,6 +1377,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 @@ -1875,8 +1921,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 @@ -1887,6 +1933,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 {parentLocationType} be the expected type of {ObjectField}'s parent + {ObjectValue}. + - If {parentLocationType} is a OneOf Input Object type, return {true}. +- Return {false}. + AreTypesCompatible(variableType, locationType): - If {locationType} is a non-null type: @@ -1975,6 +2030,34 @@ 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 + } +} +mutation addNullableCatWithDefault($cat: CatInput = { name: "Brontie" }) { + addPet(pet: { cat: $cat }) { + name + } +} +``` + **Allowing Optional Variables When Default Values Exist** A notable exception to typical variable type compatibility is allowing a