Skip to content

[RFC] Default value coercion rules #793

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

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,8 @@ of rules must be adhered to by every Object type in a GraphQL schema.
characters {"__"} (two underscores).
2. The argument must accept a type where {IsInputType(argumentType)}
returns {true}.
3. If the argument has a default value it must be compatible with
{argumentType} as per the coercion rules for that type.
3. An object type may declare that it implements one or more unique interfaces.
4. An object type must be a super-set of all interfaces it implements:
1. Let this object type be {objectType}.
Expand Down Expand Up @@ -1520,7 +1522,8 @@ defined by the input object type and for which a value exists. The resulting map
is constructed with the following rules:

* If no value is provided for a defined input object field and that field
definition provides a default value, the default value should be used. If no
definition provides a default value, the result of coercing the default value
according to the coercion rules of the input field type should be used. If no
default value is provided and the input object field's type is non-null, an
error should be thrown. Otherwise, if the field is not required, then no entry
is added to the coerced unordered map.
Expand Down Expand Up @@ -1580,6 +1583,17 @@ Literal Value | Variables | Coerced Value
characters {"__"} (two underscores).
3. The input field must accept a type where {IsInputType(inputFieldType)}
returns {true}.
4. If the input field has a non-null default value:
1. If the input field references this Input Object either directly or
through referenced Input Objects, all input fields in the chain of
references which reference this Input Object must either:
1. have no default value; or
2. have a {null} default value; or
3. have a default value, {nestedDefaultValue}, such that the value for
this field within {nestedDefaultValue} is either {null} or an empty
list.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this possibly be incorporated into the existing step 3 below?

Perhaps:

at least one of the fields in the chain of references must be either a nullable or a List type.

adds something along the lines of "and have either no default value, or a default value of either {null} or an empty list"

Copy link
Member Author

@benjie benjie Apr 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so, these constraints live at different levels. Step 3 below relates to the entire "default value" either being not present, null, or an empty list; however the text here (above) relates to the fields of the object within the default value. E.g. the following example:

input A {
  b: B = {}
}

input B {
  c: C = { a: null }
}

input C {
  a: A = {}
}

is fine since no matter whether we reference type A, B or C initially the chain will terminate at C.a since the default for B.c includes that C.a is null. Note: at no point is the default value for a field null or an empty list.

The wording could definitely do with some work though 😅

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scratch that; I think you're right but the wording you've suggested doesn't quite work (see above example). Maybe:

  1. If an Input Object references itself either directly or through referenced
    Input Objects:
    1. at least one of the fields in the chain of references must be
      either a nullable or a List type.
    2. at least one of the fields in the chain must have no default value, have a default value of either {null} or an empty list, or have a default value that results in all instances of one of the fields in the chain defaulting to {null} or an empty list)

Bit awkward wording, but hopefully you catch my drift. i.e. allow the following:

input A {
  b: B! = {}
}

input B {
  c: C = {}
}

input C {
  a: A! = { b: { c: null} }
}

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are very helpful examples! I was definitely too restrictive in my rule set. I think what I currently have implemented in PR would reject all cycles, but would also incorrectly reject this example.

I agree that wording isn't quite right, but it's much closer. I like that this has just one rule about circular references but it makes it clear that there are multiple ways to have illegal circular references - and the above rule can specifically be about type checking the default value

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to be clear here:
the coerced default value for A = {b: {c: {a: {b: {c: null}}}}}.
the coerced default value for B = {c: {a: {b: {c: null}}}}.
the coerced default value for C = {a: {b: {c: null}}}

That correct?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also: we would have to apply this validation rule also for default variable values, right?

Copy link
Collaborator

@leebyron leebyron Apr 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we also need it for variable values or argument defaults since those can only be the entry to a circular ref but not actually contained within a circular ref.

input A {
  a: A = {}
}

query Test ($x: A = {}) {}

This is problematic because A.a creates a circular reference, but $x only references it, it isn't contained in it.

I think more generally you can say that given a set of Input Objects that have no circular reference, there exists no default value for arguments or variable values that would create a circular reference.

Copy link
Collaborator

@leebyron leebyron Apr 26, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just to be clear here:
the coerced default value for A = {b: {c: {a: {b: {c: null}}}}}.
the coerced default value for B = {c: {a: {b: {c: null}}}}.
the coerced default value for C = {a: {b: {c: null}}}

That correct?

Coerced default values apply to fields, not types - so:

the coerced default value for A.b = {c: {a: {b: {c: null}}}}.
the coerced default value for B.c = {a: {b: {c: null}}}.
the coerced default value for C.a = {b: {c: null}}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay I've had a go at writing an algorithm for this; I think the key learning whilst attempting this was that the act of referencing the default value a second time is what triggers a cycle; so the algoritm only tracks when the default value of a field is referenced.

2. {defaultValue} must be compatible with {inputFieldType} as per the
coercion rules for that type.
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.
Expand Down
8 changes: 6 additions & 2 deletions spec/Section 6 -- Execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,10 @@ CoerceVariableValues(schema, operation, variableValues):
* Let {value} be the value provided in {variableValues} for the
name {variableName}.
* If {hasValue} is not {true} and {defaultValue} exists (including {null}):
* Let {coercedDefaultValue} be the result of coercing {defaultValue} according to the
input coercion rules of {variableType}.
* Add an entry to {coercedValues} named {variableName} with the
value {defaultValue}.
value {coercedDefaultValue}.
* Otherwise if {variableType} is a Non-Nullable type, and either {hasValue}
is not {true} or {value} is {null}, throw a query error.
* Otherwise if {hasValue} is true:
Expand Down Expand Up @@ -586,8 +588,10 @@ CoerceArgumentValues(objectType, field, variableValues):
name {variableName}.
* Otherwise, let {value} be {argumentValue}.
* If {hasValue} is not {true} and {defaultValue} exists (including {null}):
* Let {coercedDefaultValue} be the result of coercing {defaultValue} according to the
input coercion rules of {argumentType}.
* Add an entry to {coercedValues} named {argumentName} with the
value {defaultValue}.
value {coercedDefaultValue}.
* Otherwise if {argumentType} is a Non-Nullable type, and either {hasValue}
is not {true} or {value} is {null}, throw a field error.
* Otherwise if {hasValue} is true:
Expand Down