-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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] Default value coercion rules #793
base: main
Are you sure you want to change the base?
Conversation
An alternative approach is to apply the coercion during execution in the same way it is done for user-supplied values (the spec mods for this would look like: cb29f6e) but this could of course be done once and cached at schema build time. EDIT: the reason to do it this way would be so that the |
NOTE: we'd also have to prevent infinite recursion in default values; for example the following should throw a schema validation error: input O {
o: O = {}
a: Int
} but the following should be fine: input O {
o: O = { o: null }
a: Int
} |
Whether a server does the coercion at execution time or build time is largely an implementation detail, but I do think we should specify the more natural behaviour that comes from doing it at execution time: expected output from graphql-ruby at least already seems to try to coerce the default value, but if the coercion fails it just silently passes a null to the resolver even if the argument is required. It would be great to have this behaviour well-defined. |
This is exactly what I'm thinking too; it's why I closed my graphql-js PR because I think the solution's going to be somewhat more involved. |
spec/Section 6 -- Execution.md
Outdated
* Let {coercedDefaultValue} be the result of coercing {defaultValue} according to the | ||
input coercion rules of {argumentType}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NOTE: this is memoizeable and could be computed at schema build time (without affecting the introspection results).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TODO: add this as a non-normative note below.
Put a little more time into this spec edit, I think it's much more viable now. |
@benjie I'm all for adding validation on default values but I'm against applying coercion rules on them, because:
|
The infinite recursion during coercion problemThe following schema is (I believe) valid GraphQL currently: # SCHEMA 1
input A {
b: B = {}
}
input B {
a: A = {}
}
type Query {
q(a: A = {}): Int
} (Note: we could also do this with a self-reference In current implementations this will be fine because the default value is not coerced and thus there is no recursion. However, if we implement this RFC and ignore the "must not cause an infinite loop" part (highlighted by @andimarek) then this could cause infinite recursion during coercion. However; the following GraphQL schema would be fine as there'd be no infinite recursion: # SCHEMA 2
input A {
b: B = {}
}
input B {
a: A = {b: null}
}
type Query {
q(a: A = {}): Int
} Once we reach a situation where I think we need a schema validation rule that invalidates "SCHEMA 1". This would be a breaking change to the GraphQL spec, but I think it's an acceptable one and can be easily resolved by GraphQL schema designers by explicitly changing the defaults from I do not know how to write this change into the spec (hence the "must not cause an infinite loop" placeholder); @leebyron could you give some hints on the approach you'd take? |
@IvanGoncharov mentioned that #701 might help with the recursive defaults problem, but alas I don't think it will as currently stated - the RFC seems to relate to making sure fields are nullable to prevent expressing a state that's not representable via a JSON input, this doesn't solve the infinite recursion issue with default values above (note the fields above are all nullable). |
@IvanGoncharov in response to your feedback, we discussed this during the WG but I'll include a summary of my positions here:
For me, seeing Ultimately, it is up to the people who develop the schema to decide if this is an issue for them or not - if it is they can consider not using defaultValue at all.
Can you expand on this, and perhaps suggest how the proposal can address it/minimise it? To my mind the
Indeed, I think this is an acceptable trade-off.
This was discussed in the WG, and I feel that default value / passed value equivalence (i.e. seeing
Agree, however the fix is straightforward and it would be determined at schema validation time (not runtime) so should be quick to address for implementors. Further, if implementors are already doing what you say they should be doing (i.e. supplying the full expected default value, applying whatever defaults are needed manually) then their schemas will not be broken by this change. |
During the January working group meeting, we discussed the idea of sharing example schemas so we have a better idea of how to move forward and I mentioned that I worked on a research paper where we analyzed a large corpus (8,399) of open source schemas that we collected from GitHub. I mentioned that there may be complications with sharing this corpus but it turns out I was misremembering some of the details. We were not able to directly publish the raw schemas and "recovered" schemas (schemas formed by merging small sub-schemas), but we were able to publish links to the commit-stamped files! See here. Additionally, we also published the scripts used to collect, clean, and refactor all the schemas in the corpus. This could be used to generate a new up to date corpus from GitHub. See here for the scripts. |
@benjie asked for real examples of use. Here’s an “anonymized” example from a real schema: input Z {
x: Int = 1
y: Int = 2
}
input A {
b: Z = {x: 1, y: 2}
c: Z = {x: 1, y: 3}
}
type T {
f(a: A = {b: {x: 1, y: 2}, c: {x: 1, y: 3}}): F Note that there is really use of multiple levels of default values here. Given the spec today the |
Providing another example here from a real-world schema from Indeed's GraphQL API. We have a number of query operations that define nested input types that specify default values. The most prominent example is the core job search operation:
Note that the default values are specified inline for nested inputs. |
GitHub uses objects as defaultValues a lot in their schema for their orderBy arguments. For example, securityAdvisories(
...
orderBy: GitHubSecurityVulnerabilityOrder = {field: UPDATED_AT, direction: DESC}
...
): GitHubSecurityVulnerabilityConnection! |
A developer from a large company (household name) DM'd me on Twitter to share parts of their schema, and they have a similar pattern to GitHub for ordering; the relevant parts of their schema looks a bit like (manually anonymised): enum SortDirection {
ASC
DESC
}
input FooOrder {
sort: FooSort!
direction: SortDirection! = ASC
}
type Query {
foos(orderBy: FooOrder = {sort: DATE, direction: ASC}): FooConnection
} They also use this for conditions: enum OperatorEnum {
OR
AND
}
input BarSearchInput {
queries: [BarQuery]
queryOperator: OperatorEnum = OR
}
type Query {
barSearch(search: BarSearchInput = {queryOperator: OR}): BarConnection
} And have defaults used for input objects used in mutations: enum BazType {
CAT
HEDGEHOG
ZEBRA
}
input BazInput {
type: BazType = CAT
url: String!
name: String
}
type Mutation {
addBaz(quxId: ID!, baz: BazInput!): Baz
} |
0e0404a
to
25da056
Compare
✅ Deploy Preview for graphql-spec-draft ready!
To edit notification comments on pull requests, go to your Netlify site settings. |
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Remove `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
Implements graphql/graphql-spec#793 * Adds validation of default values during schema validation. * Adds coercion of default values anywhere a default value is used at runtime Potentially breaking: * Deprecates `astFromValue` * Changes type of `defaultValue` provided during type configuration from an "internal" to an "external" value.
[#3049 rebased on main](#3049). This is the last rebased PR from the original PR stack concluding with #3049. * Rebased: #3809 [Original: #3092] * Rebased: #3810 [Original: #3074] * Rebased: #3811 [Original: #3077] * Rebased: #3812 [Original: #3065] * Rebased: #3813 [Original: #3086] * Rebased: #3814 (this PR) [Original: #3049] Update: #3044 and #3145 have been separated from this stack. Changes from original PR: 1. `astFromValue()` is deprecated instead of being removed. @leebyron comments from #3049, the original PR: > Implements [graphql/graphql-spec#793](graphql/graphql-spec#793) > > * BREAKING: Changes default values from being represented as an assumed-coerced "internal input value" to a pre-coerced "external input value" (See chart below). > This allows programmatically provided default values to be represented in the same domain as values sent to the service via variable values, and allows it to have well defined methods for both transforming into a printed GraphQL literal string for introspection / schema printing (via `valueToLiteral()`) or coercing into an "internal input value" for use at runtime (via `coerceInputValue()`) > To support this change in value type, this PR adds two related behavioral changes: > > * Adds coercion of default values from external to internal at runtime (within `coerceInputValue()`) > * Removes `astFromValue()`, replacing it with `valueToLiteral()` for use in introspection and schema printing. `astFromValue()` performed unsafe "uncoercion" to convert an "Internal input value" directly to a "GraphQL Literal AST", where `valueToLiteral()` performs a well defined transform from "External input value" to "GraphQL Literal AST". > * Adds validation of default values during schema validation. > Since assumed-coerced "internal" values may not pass "external" validation (for example, Enum values), an additional validation error has been included which attempts to detect this case and report a strategy for resolution. > > Here's a broad overview of the intended end state: > > ![GraphQL Value Flow](https://user-images.githubusercontent.com/50130/118379946-51ac5300-b593-11eb-839f-c483ecfbc875.png) --------- Co-authored-by: Lee Byron <lee@leebyron.com>
Coercing Field Arguments states:
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:When building the following GraphQL schema programmatically (code below) with GraphQL.js:
And a resolver for
Query.example
:You might expect the following queries to all gave the same result:
However, it turns out that query A's result differs:
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
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:
But there doesn't seem to be any such assertion for GraphQL schemas defined in code.