Skip to content

Commit

Permalink
[RFC] Ordering of queried fields
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
leebyron committed Mar 21, 2016
1 parent fb100f1 commit d4b4e67
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 22 deletions.
19 changes: 19 additions & 0 deletions spec/Section 2 -- Language.md
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,25 @@ curly-braces `{ }`. The values of an object literal may be any input value
literal or variable (ex. `{ name: "Hello world", score: 1.0 }`). We refer to
literal representation of input objects as "object literals."

**Input object fields are unordered**

Input object fields may be provided in any syntactic order and maintain
identical semantic meaning.

These two queries are semantically identical:

```graphql
{
nearestThing(location: { lon: 12.43, lat: -53.211 })
}
```

```graphql
{
nearestThing(location: { lat: -53.211, lon: 12.43 })
}
```

**Semantics**

ObjectValue : { }
Expand Down
109 changes: 101 additions & 8 deletions spec/Section 3 -- Type System.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,9 @@ While Scalar types describe the leaf values of these hierarchical queries, Objec
describe the intermediate levels.

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

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

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

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

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

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

```graphql
{
name
age
name
}
```

Must only yield exactly that subset:

```js
{
"name": "Mark Zuckerberg",
"age": 30
"age": 30,
"name": "Mark Zuckerberg"
}
```

Expand Down Expand Up @@ -342,6 +342,99 @@ And will yield the subset of each object type queried:
}
```

**Field Ordering**

When querying an Object, the resulting mapping of fields are conceptually
ordered in the same order in which they were encountered during query execution,
excluding fragments for which the type does not apply and fields or
fragments that are skipped via `@skip` or `@include` directives. This ordering
is correctly produced when using the {CollectFields()} algorithm.

Response formats which support ordered maps (such as JSON) must maintain this
ordering. Response formats which do not support ordered maps may disregard
this ordering.

If a fragment is spread before other fields, the fields that fragment specifies
occur in the response before the following fields.

```graphql
{
foo
...Frag
qux
}

fragment Frag on Query {
bar
baz
}
```

Produces the ordered result:

```js
{
"foo": 1,
"bar": 2,
"baz": 3,
"qux": 4
}
```

If a field is queried multiple times in a selection, it is ordered by the first
time it is encountered. However fragments for which the type does not apply does
not affect ordering.

```graphql
{
foo
...Ignored
...Matching
bar
}

fragment Ignored on UnknownType {
qux
baz
}

fragment Matching on Query {
bar
qux
foo
}
```

Produces the ordered result:

```js
{
"foo": 1,
"bar": 2,
"qux": 3
}
```

Also, if directives result in fields being excluded, they are not considered in
the ordering of fields.

```graphql
{
foo @skip(if: true)
bar
foo
}
```

Produces the ordered result:

```js
{
"bar": 1,
"foo": 2
}
```

**Result Coercion**

Determining the result of coercing an object is the heart of the GraphQL
Expand Down
24 changes: 13 additions & 11 deletions spec/Section 6 -- Execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,20 @@ The selection set is converted to a grouped field set by calling

CollectFields(objectType, selectionSet, visitedFragments):

* Initialize {groupedFields} to an empty list of lists.
* Initialize {groupedFields} to an empty ordered list of lists.
* For each {selection} in {selectionSet};
* If {selection} provides the directive `@skip`, let {skipDirective} be that directive.
* If {skipDirective}'s {if} argument is {true}, continue with the
next {selection} in {selectionSet}.
* If {selection} provides the directive `@include`, let {includeDirective} be that directive.
* If {includeDirective}'s {if} argument is {false}, continue with the
next {selection} in {selectionSet}.
* If {selection} is a Field:
* If {selection} is a {Field}:
* Let {responseKey} be the response key of {selection}.
* Let {groupForResponseKey} be the list in {groupedFields} for
{responseKey}; if no such list exists, create it as an empty list.
* Append {selection} to the {groupForResponseKey}.
* If {selection} is a FragmentSpread:
* If {selection} is a {FragmentSpread}:
* Let {fragmentSpreadName} be the name of {selection}.
* If {fragmentSpreadName} is in {visitedFragments}, continue with the
next {selection} in {selectionSet}.
Expand All @@ -76,20 +76,20 @@ CollectFields(objectType, selectionSet, visitedFragments):
* If {doesFragmentTypeApply(objectType, fragmentType)} is false, continue
with the next {selection} in {selectionSet}.
* Let {fragmentSelectionSet} be the top-level selection set of {fragment}.
* Let {fragmentGroupedFields} be the result of calling
{CollectFields(objectType, fragmentSelectionSet)}.
* For each {fragmentGroup} in {fragmentGroupedFields}:
* Let {fragmentGroupedFieldSet} be the result of calling
{CollectFields(objectType, fragmentSelectionSet, visitedFragments)}.
* For each {fragmentGroup} in {fragmentGroupedFieldSet}:
* Let {responseKey} be the response key shared by all fields in {fragmentGroup}
* Let {groupForResponseKey} be the list in {groupedFields} for
{responseKey}; if no such list exists, create it as an empty list.
* Append all items in {fragmentGroup} to {groupForResponseKey}.
* If {selection} is an inline fragment:
* If {selection} is an {InlineFragment}:
* Let {fragmentType} be the type condition on {selection}.
* If {fragmentType} is not {null} and {doesFragmentTypeApply(objectType, fragmentType)} is false, continue
with the next {selection} in {selectionSet}.
* Let {fragmentSelectionSet} be the top-level selection set of {selection}.
* Let {fragmentGroupedFields} be the result of calling {CollectFields(objectType, fragmentSelectionSet)}.
* For each {fragmentGroup} in {fragmentGroupedFields}:
* Let {fragmentGroupedFieldSet} be the result of calling {CollectFields(objectType, fragmentSelectionSet, visitedFragments)}.
* For each {fragmentGroup} in {fragmentGroupedFieldSet}:
* Let {responseKey} be the response key shared by all fields in {fragmentGroup}
* Let {groupForResponseKey} be the list in {groupedFields} for
{responseKey}; if no such list exists, create it as an empty list.
Expand All @@ -112,8 +112,10 @@ it should be evaluated normally.

## Evaluating a grouped field set

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

### Field entries

Expand Down
10 changes: 7 additions & 3 deletions spec/Section 7 -- Response.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ representations of the following four primitives:
* String
* Null

Serialization formats which only support an ordered map (such as JSON) must
preserve ordering as it is defined by query execution. Serialization formats
which only support an unordered map may omit this ordering information.

A serialization format may support the following primitives, however, strings
may be used as a substitute for those primitives.

Expand Down Expand Up @@ -52,13 +56,13 @@ the following JSON concepts:

A response to a GraphQL operation must be a map.

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

If the operation encountered any errors, the response map must contain an entry
with key `errors`. The value of this entry is described in the "Errors"
If the operation encountered any errors, the response map must contain a next
entry with key `errors`. The value of this entry is described in the "Errors"
section. If the operation completed without encountering any errors, this entry
must not be present.

Expand Down

0 comments on commit d4b4e67

Please sign in to comment.