Skip to content

Commit d4b4e67

Browse files
committed
[RFC] Ordering of queried fields
This removes language referring to responses as unordered. In fact responses should always be ordered in the order in which they were queried. This adds clarifying language around both object types and input objects about ordering and provides more examples. I've also added a caveat that response formats which don't support ordering are not required to uphold it. **Impact:** Some servers may return responses in an unordered fashion due to the way their executor behaves, though most which are ported from or inspired by graphql-js, our reference implementation, already behave this way. **Motivation:** This enables custom parsers which expect JSON values to be a very specific shape for performance reasons, but it also ensures an easier level of understandability when a response's shape so tightly matches the query - both for new learners and for debugging.
1 parent fb100f1 commit d4b4e67

File tree

4 files changed

+140
-22
lines changed

4 files changed

+140
-22
lines changed

spec/Section 2 -- Language.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,25 @@ curly-braces `{ }`. The values of an object literal may be any input value
779779
literal or variable (ex. `{ name: "Hello world", score: 1.0 }`). We refer to
780780
literal representation of input objects as "object literals."
781781

782+
**Input object fields are unordered**
783+
784+
Input object fields may be provided in any syntactic order and maintain
785+
identical semantic meaning.
786+
787+
These two queries are semantically identical:
788+
789+
```graphql
790+
{
791+
nearestThing(location: { lon: 12.43, lat: -53.211 })
792+
}
793+
```
794+
795+
```graphql
796+
{
797+
nearestThing(location: { lat: -53.211, lon: 12.43 })
798+
}
799+
```
800+
782801
**Semantics**
783802

784803
ObjectValue : { }

spec/Section 3 -- Type System.md

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,9 @@ While Scalar types describe the leaf values of these hierarchical queries, Objec
234234
describe the intermediate levels.
235235

236236
GraphQL Objects represent a list of named fields, each of which yield a value of
237-
a specific type. Object values are serialized as unordered maps, where the
237+
a specific type. Object values are serialized as ordered maps, where the
238238
queried field names (or aliases) are the keys and the result of evaluating
239-
the field is the value.
239+
the field is the value, ordered by the order in which they appear in the query.
240240

241241
For example, a type `Person` could be described as:
242242

@@ -253,9 +253,9 @@ that will yield an `Int` value, and `picture` a field that will yield a
253253
`Url` value.
254254

255255
A query of an object value must select at least one field. This selection of
256-
fields will yield an unordered map containing exactly the subset of the object
257-
queried. Only fields that are declared on the object type may validly be queried
258-
on that object.
256+
fields will yield an ordered map containing exactly the subset of the object
257+
queried, in the order in which they were queried. Only fields that are declared
258+
on the object type may validly be queried on that object.
259259

260260
For example, selecting all the fields of `Person`:
261261

@@ -281,17 +281,17 @@ While selecting a subset of fields:
281281

282282
```graphql
283283
{
284-
name
285284
age
285+
name
286286
}
287287
```
288288

289289
Must only yield exactly that subset:
290290

291291
```js
292292
{
293-
"name": "Mark Zuckerberg",
294-
"age": 30
293+
"age": 30,
294+
"name": "Mark Zuckerberg"
295295
}
296296
```
297297

@@ -342,6 +342,99 @@ And will yield the subset of each object type queried:
342342
}
343343
```
344344

345+
**Field Ordering**
346+
347+
When querying an Object, the resulting mapping of fields are conceptually
348+
ordered in the same order in which they were encountered during query execution,
349+
excluding fragments for which the type does not apply and fields or
350+
fragments that are skipped via `@skip` or `@include` directives. This ordering
351+
is correctly produced when using the {CollectFields()} algorithm.
352+
353+
Response formats which support ordered maps (such as JSON) must maintain this
354+
ordering. Response formats which do not support ordered maps may disregard
355+
this ordering.
356+
357+
If a fragment is spread before other fields, the fields that fragment specifies
358+
occur in the response before the following fields.
359+
360+
```graphql
361+
{
362+
foo
363+
...Frag
364+
qux
365+
}
366+
367+
fragment Frag on Query {
368+
bar
369+
baz
370+
}
371+
```
372+
373+
Produces the ordered result:
374+
375+
```js
376+
{
377+
"foo": 1,
378+
"bar": 2,
379+
"baz": 3,
380+
"qux": 4
381+
}
382+
```
383+
384+
If a field is queried multiple times in a selection, it is ordered by the first
385+
time it is encountered. However fragments for which the type does not apply does
386+
not affect ordering.
387+
388+
```graphql
389+
{
390+
foo
391+
...Ignored
392+
...Matching
393+
bar
394+
}
395+
396+
fragment Ignored on UnknownType {
397+
qux
398+
baz
399+
}
400+
401+
fragment Matching on Query {
402+
bar
403+
qux
404+
foo
405+
}
406+
```
407+
408+
Produces the ordered result:
409+
410+
```js
411+
{
412+
"foo": 1,
413+
"bar": 2,
414+
"qux": 3
415+
}
416+
```
417+
418+
Also, if directives result in fields being excluded, they are not considered in
419+
the ordering of fields.
420+
421+
```graphql
422+
{
423+
foo @skip(if: true)
424+
bar
425+
foo
426+
}
427+
```
428+
429+
Produces the ordered result:
430+
431+
```js
432+
{
433+
"bar": 1,
434+
"foo": 2
435+
}
436+
```
437+
345438
**Result Coercion**
346439

347440
Determining the result of coercing an object is the heart of the GraphQL

spec/Section 6 -- Execution.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,20 @@ The selection set is converted to a grouped field set by calling
5050

5151
CollectFields(objectType, selectionSet, visitedFragments):
5252

53-
* Initialize {groupedFields} to an empty list of lists.
53+
* Initialize {groupedFields} to an empty ordered list of lists.
5454
* For each {selection} in {selectionSet};
5555
* If {selection} provides the directive `@skip`, let {skipDirective} be that directive.
5656
* If {skipDirective}'s {if} argument is {true}, continue with the
5757
next {selection} in {selectionSet}.
5858
* If {selection} provides the directive `@include`, let {includeDirective} be that directive.
5959
* If {includeDirective}'s {if} argument is {false}, continue with the
6060
next {selection} in {selectionSet}.
61-
* If {selection} is a Field:
61+
* If {selection} is a {Field}:
6262
* Let {responseKey} be the response key of {selection}.
6363
* Let {groupForResponseKey} be the list in {groupedFields} for
6464
{responseKey}; if no such list exists, create it as an empty list.
6565
* Append {selection} to the {groupForResponseKey}.
66-
* If {selection} is a FragmentSpread:
66+
* If {selection} is a {FragmentSpread}:
6767
* Let {fragmentSpreadName} be the name of {selection}.
6868
* If {fragmentSpreadName} is in {visitedFragments}, continue with the
6969
next {selection} in {selectionSet}.
@@ -76,20 +76,20 @@ CollectFields(objectType, selectionSet, visitedFragments):
7676
* If {doesFragmentTypeApply(objectType, fragmentType)} is false, continue
7777
with the next {selection} in {selectionSet}.
7878
* Let {fragmentSelectionSet} be the top-level selection set of {fragment}.
79-
* Let {fragmentGroupedFields} be the result of calling
80-
{CollectFields(objectType, fragmentSelectionSet)}.
81-
* For each {fragmentGroup} in {fragmentGroupedFields}:
79+
* Let {fragmentGroupedFieldSet} be the result of calling
80+
{CollectFields(objectType, fragmentSelectionSet, visitedFragments)}.
81+
* For each {fragmentGroup} in {fragmentGroupedFieldSet}:
8282
* Let {responseKey} be the response key shared by all fields in {fragmentGroup}
8383
* Let {groupForResponseKey} be the list in {groupedFields} for
8484
{responseKey}; if no such list exists, create it as an empty list.
8585
* Append all items in {fragmentGroup} to {groupForResponseKey}.
86-
* If {selection} is an inline fragment:
86+
* If {selection} is an {InlineFragment}:
8787
* Let {fragmentType} be the type condition on {selection}.
8888
* If {fragmentType} is not {null} and {doesFragmentTypeApply(objectType, fragmentType)} is false, continue
8989
with the next {selection} in {selectionSet}.
9090
* Let {fragmentSelectionSet} be the top-level selection set of {selection}.
91-
* Let {fragmentGroupedFields} be the result of calling {CollectFields(objectType, fragmentSelectionSet)}.
92-
* For each {fragmentGroup} in {fragmentGroupedFields}:
91+
* Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, visitedFragments)}.
92+
* For each {fragmentGroup} in {fragmentGroupedFieldSet}:
9393
* Let {responseKey} be the response key shared by all fields in {fragmentGroup}
9494
* Let {groupForResponseKey} be the list in {groupedFields} for
9595
{responseKey}; if no such list exists, create it as an empty list.
@@ -112,8 +112,10 @@ it should be evaluated normally.
112112

113113
## Evaluating a grouped field set
114114

115-
The result of evaluating a grouped field set will be an unordered map. There
116-
will be an entry in this map for every item in the grouped field set.
115+
The result of evaluating a grouped field set will be an ordered map. For each
116+
item in the grouped field set, an entry is added to the resulting ordered map,
117+
where the key is the response key shared by all fields for that entry, and the
118+
value is the result of evaluating those fields.
117119

118120
### Field entries
119121

spec/Section 7 -- Response.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ representations of the following four primitives:
2020
* String
2121
* Null
2222

23+
Serialization formats which only support an ordered map (such as JSON) must
24+
preserve ordering as it is defined by query execution. Serialization formats
25+
which only support an unordered map may omit this ordering information.
26+
2327
A serialization format may support the following primitives, however, strings
2428
may be used as a substitute for those primitives.
2529

@@ -52,13 +56,13 @@ the following JSON concepts:
5256

5357
A response to a GraphQL operation must be a map.
5458

55-
If the operation included execution, the response map must contain an entry
59+
If the operation included execution, the response map must contain a first entry
5660
with key `data`. The value of this entry is described in the "Data" section. If
5761
the operation failed before execution, due to a syntax error, missing
5862
information, or validation error, this entry must not be present.
5963

60-
If the operation encountered any errors, the response map must contain an entry
61-
with key `errors`. The value of this entry is described in the "Errors"
64+
If the operation encountered any errors, the response map must contain a next
65+
entry with key `errors`. The value of this entry is described in the "Errors"
6266
section. If the operation completed without encountering any errors, this entry
6367
must not be present.
6468

0 commit comments

Comments
 (0)