Default value coercion rules
At a glance​
- Identifier: #793
- Stage: RFC2: Draft
- Champion: @benjie
- PR: Default value coercion rules
Timeline​
- Added to 2023-12-07 WG agenda
- Mentioned in 2023-12 WG notes
- Added to 2023-06-01 WG agenda
- Mentioned in 2023-06 WG notes
- Added to 2023-03-02 WG agenda
- Mentioned in 2023-03 WG notes
- Added to 2023-02-02 WG agenda
- Mentioned in 2023-02 WG notes
- Commit pushed: Merge branch 'main' into input-object-default-value on 2023-01-31 by @benjie
- 5 commits pushed on 2022-01-06:
- Added to 2022-01-06 WG agenda
- Mentioned in 2022-01-06 WG notes
- Added to 2021-05-13 WG agenda
- Mentioned in 2021-05-13 WG notes
- Commit pushed: Clarify the default value cycle detection logic on 2021-05-03 by @benjie
- Added to 2021-03-04 WG agenda
- Mentioned in 2021-03-04 WG notes
- 2 commits pushed on 2021-02-26:
- Commit pushed: Fix 'must not cause an infinite loop' wording. on 2021-02-19 by @benjie
- Added to 2021-01-07 WG agenda
- Mentioned in 2021-01-07 WG notes
- Added to 2020-12-03 WG agenda
- Mentioned in 2020-12-03 WG notes
- Spec PR created on 2020-11-13 by benjie
- Commit pushed: Default value coercion rules on 2020-11-13 by @benjie
Coercing Field Arguments states:
5.c. Let defaultValue be the default value for argumentDefinition. [...] 5.h. If hasValue is not true and defaultValue exists (including null): 5.h.i. Add an entry to coercedValues named argumentName with the value defaultValue. [...] 5.j.iii.1. If value cannot be coerced according to the input coercion rules of argumentType, throw a field error. 5.j.iii.2. Let coercedValue be the result of coercing value according to the input coercion rules of argumentType. 5.j.iii.3. Add an entry to coercedValues named argumentName with the value coercedValue.
Here we note that there is no run-time coercion of
defaultValue
, which makes sense (why do at runtime that which can be done at build time?). However, there doesn't seem to be a rule that specifies thatdefaultValue
must be coerced at all, which leads to consequences:
- you could use any value for defaultValue and it could break the type safety guarantees of GraphQL
- nested defaultValues are not applied
When building the following GraphQL schema programmatically (code below) with GraphQL.js:
type Query {
example(inputObject: ExampleInputObject! = {}): Int
}
input ExampleInputObject {
number: Int! = 3
}And a resolver for
Query.example
:resolve(source, args) {
return args.inputObject.number;
}You might expect the following queries to all gave the same result:
query A {
example
}
query B {
example(inputObject: {})
}
query C {
example(inputObject: { number: 3 })
}
query D($inputObject: ExampleInputObject! = {}) {
example(inputObject: $inputObject)
}However, it turns out that query A's result differs:
{"example":null}
{"example":3}
{"example":3}
{"example":3}This is because
defaultValue
forQuery.example(inputObject:)
was not coerced, so none of the defaultValues ofExampleInputObject
were applied.This is extremely unexpected, because looking at the GraphQL schema definition it looks like there's no circumstance under which
ExampleInputObject
may not havenumber
as an integer; however when thedefaultValue
ofQuery.example(inputObject:)
is used, the value ofnumber
isundefined
.Example runnable with GraphQL.js
const {
graphqlSync,
printSchema,
GraphQLSchema,
GraphQLObjectType,
GraphQLInputObjectType,
GraphQLInt,
GraphQLNonNull,
} = require("graphql");
const ExampleInputObject = new GraphQLInputObjectType({
name: "ExampleInputObject",
fields: {
number: {
type: new GraphQLNonNull(GraphQLInt),
defaultValue: 3,
},
},
});
const Query = new GraphQLObjectType({
name: "Query",
fields: {
example: {
args: {
inputObject: {
type: new GraphQLNonNull(ExampleInputObject),
defaultValue: {},
},
},
type: GraphQLInt,
resolve(source, args) {
return args.inputObject.number;
},
},
},
});
const schema = new GraphQLSchema({
query: Query,
});
console.log(printSchema(schema));
// All four of these should be equivalent?
const source = /* GraphQL */ `
query A {
example
}
query B {
example(inputObject: {})
}
query C {
example(inputObject: { number: 3 })
}
query D($inputObject: ExampleInputObject! = {}) {
example(inputObject: $inputObject)
}
`;
const result1 = graphqlSync({ schema, source, operationName: "A" });
const result2 = graphqlSync({ schema, source, operationName: "B" });
const result3 = graphqlSync({ schema, source, operationName: "C" });
const result4 = graphqlSync({ schema, source, operationName: "D" });
console.log(JSON.stringify(result1.data));
console.log(JSON.stringify(result2.data));
console.log(JSON.stringify(result3.data));
console.log(JSON.stringify(result4.data));This was raised against GraphQL.js back in 2016, but @IvanGoncharov closed it early last year stating that GraphQL.js conforms to the GraphQL Spec in this regard.
My proposal is that when a
defaultValue
is specified, the GraphQL implementation should coerce it to conform to the relevant type just like it does for runtime values as specified in Coercing Variable Values and Coercing Field Arguments.
This is validated for query documents (and schema defined as SDL), because:
Literal values must be compatible with the type expected in the position they are found as per the coercion rules defined in the Type System chapter. -- http://spec.graphql.org/draft/#sel-FALXDFDDAACFAhuF
But there doesn't seem to be any such assertion for GraphQL schemas defined in code.