Skip to content

Commit d388f13

Browse files
authored
Update Input Unions RFC (#790)
1 parent 3b8a74b commit d388f13

File tree

1 file changed

+199
-67
lines changed

1 file changed

+199
-67
lines changed

rfcs/InputUnion.md

Lines changed: 199 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,9 @@ Criteria have been given a "score" according to their relative importance in sol
232232

233233
The premise of this RFC - GraphQL should contain a polymorphic Input type.
234234

235-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
236-
|----|----|----|----|----|
237-
||||||
235+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
236+
|----|----|----|----|----|----|----|
237+
||||||||
238238

239239
Criteria score: 🥇
240240

@@ -244,9 +244,9 @@ Any data structure that can be modeled with output type polymorphism should be a
244244

245245
* ✂️ Objection: composite input types and composite output types are distinct. Fields on composite output types support aliases and arguments whereas fields on composite input types do not. Marking an output field as non-nullable is a non-breaking change, but marking an input field as non-nullable is a breaking change.
246246

247-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
248-
|----|----|----|----|----|
249-
| ✅⚠️ ||| ✅⚠️ | 🚫 |
247+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
248+
|----|----|----|----|----|----|----|
249+
| ✅⚠️ ||| ✅⚠️ | 🚫 || ✅⚠️ |
250250

251251
Criteria score: 🥇
252252

@@ -257,19 +257,19 @@ https://graphql.github.io/graphql-spec/draft/#sec-Validation.Type-system-evoluti
257257

258258
Adding a new member type to an Input Union or doing any non-breaking change to existing member types does not result in breaking change. For example, adding a new optional field to member type or changing a field from non-nullable to nullable does not break previously valid client operations.
259259

260-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
261-
|----|----|----|----|----|
262-
|| ✅⚠️ | 🚫 | ⚠️ ||
260+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
261+
|----|----|----|----|----|----|----|
262+
|| ✅⚠️ | 🚫 | ⚠️ ||||
263263

264264
Criteria score: 🥇
265265

266266
## 🎯 D. Any member type restrictions are validated in schema
267267

268268
If a solution places any restrictions on member types, compliance with these restrictions should be fully validated during schema building (analagous to how interfaces enforce restrictions on member types).
269269

270-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
271-
|----|----|----|----|----|
272-
||||||
270+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
271+
|----|----|----|----|----|----|----|
272+
||||||||
273273

274274
Criteria score: 🥇
275275

@@ -281,9 +281,9 @@ In addition to containing Input types, member type may also contain Leaf types l
281281
* Potential solution: only allow a single built-in leaf type per input union.
282282
* ✂️ Objection: Output polymorphism is restricted to Object types only. Supporting Leaf types in Input polymorphism would create a new inconsistency.
283283

284-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
285-
|----|----|----|----|----|
286-
| 🚫 | 🚫 | ✅⚠️ | 🚫 ||
284+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
285+
|----|----|----|----|----|----|----|
286+
| 🚫 | 🚫 | ✅⚠️ | 🚫 ||||
287287

288288
Criteria score: 🥉
289289

@@ -303,9 +303,9 @@ input union IU = I | { y: Int }
303303
* ✂️ Objection: achieving this by indicating the default in the union (either explicitly or implicitly via the order) is undesirable as it may require multiple equivalent unions being created where only the default differs.
304304
* ✂️ Objection: Numerous changes to a schema currently introduce breaking changes. The possibility of a breaking change isn't a breaking change and shouldn't prevent a polymorphic input type from existing.
305305

306-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
307-
|----|----|----|----|----|
308-
| ✅⚠️ | ✅⚠️ || ⚠️ | 🚫 |
306+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
307+
|----|----|----|----|----|----|----|
308+
| ✅⚠️ | ✅⚠️ || ⚠️ | 🚫 || 🚫 |
309309

310310
Criteria score: 🥉
311311

@@ -315,9 +315,9 @@ To ease development.
315315

316316
* ✂️ Objection: Adds complexity without enabling any new use cases.
317317

318-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
319-
|----|----|----|----|----|
320-
||||| |
318+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
319+
|----|----|----|----|----|----|----|
320+
||||| || |
321321

322322
Criteria score: X (not considered)
323323

@@ -329,9 +329,9 @@ In other words: data should require minimal or no transformation and metadata ov
329329

330330
* ✂️ Objection: This is a matter of taste - legitimate [Prior Art](#-prior-art) exists that require formatting / extra metadata.
331331

332-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
333-
|----|----|----|----|----|
334-
| ⚠️ | ⚠️ ||| ⚠️ |
332+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
333+
|----|----|----|----|----|----|----|
334+
| ⚠️ | ⚠️ ||| ⚠️ || ⚠️ |
335335

336336
Criteria score: 🥉
337337

@@ -352,19 +352,19 @@ input union IU = { x: String } | { y: Int }
352352
* ✂️ Objection: May break variable names? Only avoided with care
353353
* ✂️ Objection: There are different ways people are working around the lack of input unions so it likely won't be feasible to come up with a non-breaking migration path for all of them.
354354

355-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
356-
|----|----|----|----|----|
357-
|||| ⚠️ ||
355+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
356+
|----|----|----|----|----|----|----|
357+
|||| ⚠️ ||||
358358

359359
Criteria score: 🥉
360360

361361
## 🎯 J. A GraphQL schema that supports input unions can be queried by older GraphQL clients
362362

363363
Preferably without a loss of or change in previously supported functionality.
364364

365-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
366-
|----|----|----|----|----|
367-
||||||
365+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
366+
|----|----|----|----|----|----|----|
367+
||||||||
368368

369369
Criteria score: 🥇
370370

@@ -377,19 +377,19 @@ The less typing and fewer bytes transmitted, the better.
377377
* ✂️ Objection: The quantity of "typing" isn't a worthwhile metric, most interactions with an API are programmatic.
378378
* ✂️ Objection: Simply compressing an HTTP request will reduce the bytes transmitted more than anything having to do with the structure of a Schema.
379379

380-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
381-
|----|----|----|----|----|
382-
|| ⚠️ ||||
380+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
381+
|----|----|----|----|----|----|----|
382+
|| ⚠️ ||||||
383383

384384
Criteria score: 🥉
385385

386386
## 🎯 L. Input unions should be performant for servers
387387

388388
Ideally a server does not have to do much computation to determine which concrete type is represented by an input.
389389

390-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
391-
|----|----|----|----|----|
392-
||| ⚠️ | ⚠️ ||
390+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
391+
|----|----|----|----|----|----|----|
392+
||| ⚠️ | ⚠️ ||||
393393

394394
Criteria score: 🥉
395395

@@ -400,19 +400,19 @@ Common tools that parse GraphQL SDL should not fail when pointed at a schema whi
400400
* ✂️ Objection: Evolution of the SDL is expected with new features.
401401
* ✂️ Objection: SDL syntax error can be a positive as a "fail fast" if a system doesn't know about input unions.
402402

403-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
404-
|----|----|----|----|----|
405-
| 🚫 | 🚫 | 🚫 | 🚫 ||
403+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
404+
|----|----|----|----|----|----|----|
405+
| 🚫 | 🚫 | 🚫 | 🚫 ||| 🚫 |
406406

407407
Criteria score: X (rejected)
408408

409409
## 🎯 N. Existing code generated tooling is backwards compatible with Introspection additions
410410

411411
For example, GraphiQL should successfully render when pointed at a schema which contains polymorphic input types. It should continue to function even if it can't support the polymorphic input type.
412412

413-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
414-
|----|----|----|----|----|
415-
| ✅⚠️ | ✅⚠️ | ✅⚠️ | ✅⚠️ ||
413+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
414+
|----|----|----|----|----|----|----|
415+
| ✅⚠️ | ✅⚠️ | ✅⚠️ | ✅⚠️ ||| ✅⚠️ |
416416

417417
Criteria score: 🥈
418418

@@ -421,9 +421,9 @@ Criteria score: 🥈
421421
It should be possible to combine existing or new input types to unions freely and with ease.
422422
Adding an input to one or more unions should not require extraneous changes, constrain or be constrained by schema design.
423423

424-
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
425-
|----|----|----|----|----|
426-
| ✅️ | 🚫️ || 🚫 ||
424+
| [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7]
425+
|----|----|----|----|----|----|----|
426+
| ✅️ | 🚫️ || 🚫 ||||
427427

428428
Criteria score: 🥇
429429

@@ -865,6 +865,8 @@ type Mutation {
865865
* ✅ Any GraphQL type may be used
866866
* [F. Migrating a field to a polymorphic input type is non-breaking][criteria-f]
867867
* 🚫 Previously-valid inputs now need to be wrapped in a container object
868+
* [G. Input unions may include other input unions][criteria-g]
869+
* ✅ Any GraphQL type may be used, including other tagged types and wrapper types such as list
868870
* [H. Input unions should accept plain data][criteria-h]
869871
* ⚠️ The data is wrapped in a (simple) container type
870872
* [I. Input unions should be easy to upgrade from existing solutions][criteria-i]
@@ -891,27 +893,152 @@ type Mutation {
891893

892894
[The full spec changes can be seen here](https://github.com/graphql/graphql-spec/pull/586/files).
893895

896+
## 💡 6. Pending
897+
898+
Calls within the Input Union working group proposed a new solution, solution 6,
899+
which is a combination of features from solutions 1-4. It has not been fully
900+
formalized yet as the working group felt that the Tagged Type was more
901+
promising at this stage. This section is left as a placeholder for solution 6
902+
to be formally evaluated at a later time.
903+
904+
For some of the notes we took during the calls, see:
905+
https://github.com/graphql/graphql-wg/issues/426#issuecomment-685636596
906+
907+
For the calls themselves, see:
908+
https://www.youtube.com/watch?v=u2dnnpKEHZM&list=PLP1igyLx8foH4M0YAbVqpSo2fS1ElvNVD
909+
910+
## 💡 7. Tagged Type
911+
912+
**Champion:** @benjie
913+
914+
This solution was presented in:
915+
916+
* https://github.com/graphql/graphql-spec/pull/733
917+
918+
It's the spiritual successor of [Solution 5 - the @oneOf
919+
directive](#solution-5) after extensive feedback from the Input Unions working
920+
group.
921+
922+
In this solution, a new type is introduced to the GraphQL type system: the
923+
tagged type. The tagged type has two forms: a `tagged input` (valid only in
924+
inputs) and a `tagged output` (valid only in outputs), but the definitions look
925+
identical otherwise.
926+
927+
These tagged types define a list of member fields, exactly one of which must be
928+
present.
929+
930+
```graphql
931+
input CatInput {
932+
name: String!
933+
age: Int!
934+
livesLeft: Int
935+
}
936+
input DogInput {
937+
name: String!
938+
age: Int!
939+
breed: DogBreed
940+
}
941+
942+
tagged input AnimalInput {
943+
cat: CatInput!
944+
dog: DogInput!
945+
}
946+
947+
type Mutation {
948+
logAnimalDropOff(location: String, animals: [AnimalInput!]!): Int
949+
}
950+
951+
# Variables:
952+
{
953+
location: "Portland, OR",
954+
animals: [
955+
{
956+
cat: {
957+
name: "Buster",
958+
livesLeft: 7
959+
}
960+
}
961+
]
962+
}
963+
```
964+
965+
There's controversy over whether the `tagged output` should be introduced or
966+
not, more details on this can be read in
967+
https://github.com/graphql/graphql-spec/pull/733
968+
969+
### ⚖️ Evaluation
970+
971+
* [A. GraphQL should contain a polymorphic Input type][criteria-a]
972+
* ✅ Tagged is a valid version of a polymorphic type
973+
* [B. Input polymorphism matches output polymorphism][criteria-b]
974+
* ✅ When `tagged input` and `tagged output` are both included into the
975+
spec, a `tagged output` can have a mirrored `tagged input` in a similar way
976+
that a `type` can have a mirrored `input`
977+
* ⚠️ There's controversy over whether `tagged output` should be included in
978+
the spec as it causes confusion as to when to use union, interface, or
979+
tagged
980+
* [C. Doesn't inhibit schema evolution][criteria-c]
981+
* ✅ This technique is an evolution of a technique already in use in many schemas
982+
* [D. Any member type restrictions are validated in schema][criteria-d]
983+
* ✅ Tagged member fields are well defined
984+
* [E. A member type may be a Leaf type][criteria-e]
985+
* ✅ Any GraphQL type may be used, including other tagged types and wrapper types such as list
986+
* [F. Migrating a field to a polymorphic input type is non-breaking][criteria-f]
987+
* 🚫 Previously-valid inputs now need to be wrapped in a container object
988+
* [G. Input unions may include other input unions][criteria-g]
989+
* ✅ Any GraphQL type may be used, including other tagged types and wrapper types such as list
990+
* [H. Input unions should accept plain data][criteria-h]
991+
* ⚠️ The data is wrapped in a (simple) container type
992+
* [I. Input unions should be easy to upgrade from existing solutions][criteria-i]
993+
* ✅ This pattern is already possible, existing tagged inputs can be converted to Tagged type
994+
* [J. A GraphQL schema that supports input unions can be queried by older GraphQL clients][criteria-j]
995+
* ✅ Changes are additive only
996+
* [K. Input unions should be expressed efficiently in the query and on the wire][criteria-k]
997+
* ✅ Indication of the type can be done in 6 additional JSON characters per value (e.g. `{"a":VALUE_HERE}`) and would compress easily.
998+
* [L. Input unions should be performant for servers][criteria-l]
999+
* ✅ Type is easily determined by looking up the specified field name
1000+
* [M. Existing SDL parsers are backwards compatible with SDL additions][criteria-m]
1001+
* 🚫 New keywords are introduced
1002+
* [N. Existing code generated tooling is backwards compatible with Introspection additions][criteria-n]
1003+
* ✅⚠️
1004+
* [O. Unconstrained combination of input types to unions][criteria-o]
1005+
* ✅ Adding or removing member fields to a tagged type requires no extraneous effort and has no non-local consequences
1006+
1007+
### Summary of spec changes
1008+
1009+
- **SDL**: introduce new `tagged input` and `tagged output` definitions,
1010+
including member fields
1011+
- **Introspection**: add new type and `__Type.memberFields` field to relate to
1012+
these fields, and `__Type.isInputType`/`__Type.isOutputType` fields to
1013+
differentiate input versus output tagged types
1014+
- **Schema validation**: tagged types must contain only types that are
1015+
compatible (matching input or output) and must contain at least one field
1016+
- **Operation validation**: when validating a tagged input type, assert that
1017+
exactly one field was specified
1018+
1019+
[The full spec changes can be seen here](https://github.com/graphql/graphql-spec/pull/733/files).
1020+
8941021
# 🏆 Evaluation Overview
8951022

8961023
A quick glance at the evaluation results. Remember that passing or failing a specific criteria is NOT the final word.
8971024

898-
| | [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] |
899-
| -- | -- | -- | -- | -- | -- |
900-
| [A][criteria-a] 🥇 | ✅ | ✅ | ✅ | ✅ | ✅ |
901-
| [B][criteria-b] 🥇 | ✅⚠️ | ✅ | ✅ | ✅⚠️ | 🚫 |
902-
| [C][criteria-c] 🥇 | ✅ | ✅⚠️ | 🚫 | ⚠️ | ✅ |
903-
| [D][criteria-d] 🥇 | ✅ | ✅ | ✅ | ✅ | ✅ |
904-
| [E][criteria-e] 🥉 | 🚫 | 🚫 | ✅⚠️ | 🚫 | ✅ |
905-
| [F][criteria-f] 🥉 | ✅⚠️ | ✅⚠️ | ✅ | ⚠️ | 🚫 |
906-
| [G][criteria-g] 🥉 | ❔ | ❔ | ❔ | ❔ | ❔ |
907-
| [H][criteria-h] 🥉 | ⚠️ | ⚠️ | ✅ | ✅ | ⚠️ |
908-
| [I][criteria-i] 🥉 | ✅ | ✅ | ✅ | ⚠️ | ✅ |
909-
| [J][criteria-j] 🥇 | ✅ | ✅ | ✅ | ✅ | ✅ |
910-
| [K][criteria-k] 🥉 | ✅ | ⚠️ | ✅ | ✅ | ✅ |
911-
| [L][criteria-l] 🥉 | ✅ | ✅ | ⚠️ | ⚠️ | ✅ |
912-
| [M][criteria-m] 🥈 | 🚫 | 🚫 | 🚫 | 🚫 | ✅ |
913-
| [N][criteria-n] 🥈 | ✅⚠️ | ✅⚠️ | ✅⚠️ | ✅⚠️ | ✅ |
914-
| [O][criteria-o] 🥈 | ✅️ | 🚫️ | ❔ | 🚫 | ✅ |
1025+
| | [1][solution-1] | [2][solution-2] | [3][solution-3] | [4][solution-4] | [5][solution-5] | [6][solution-6] | [7][solution-7] |
1026+
| -- | -- | -- | -- | -- | -- | -- | -- |
1027+
| [A][criteria-a] 🥇 | ✅ | ✅ | ✅ | ✅ | ✅ | ? | ✅ |
1028+
| [B][criteria-b] 🥇 | ✅⚠️ | ✅ | ✅ | ✅⚠️ | 🚫 | ? | ✅⚠️ |
1029+
| [C][criteria-c] 🥇 | ✅ | ✅⚠️ | 🚫 | ⚠️ | ✅ | ? | ✅ |
1030+
| [D][criteria-d] 🥇 | ✅ | ✅ | ✅ | ✅ | ✅ | ? | ✅ |
1031+
| [E][criteria-e] 🥉 | 🚫 | 🚫 | ✅⚠️ | 🚫 | ✅ | ? | ✅ |
1032+
| [F][criteria-f] 🥉 | ✅⚠️ | ✅⚠️ | ✅ | ⚠️ | 🚫 | ? | 🚫 |
1033+
| [G][criteria-g] 🥉 | ❔ | ❔ | ❔ | ❔ | ❔ | ? | ✅ |
1034+
| [H][criteria-h] 🥉 | ⚠️ | ⚠️ | ✅ | ✅ | ⚠️ | ? | ⚠️ |
1035+
| [I][criteria-i] 🥉 | ✅ | ✅ | ✅ | ⚠️ | ✅ | ? | ✅ |
1036+
| [J][criteria-j] 🥇 | ✅ | ✅ | ✅ | ✅ | ✅ | ? | ✅ |
1037+
| [K][criteria-k] 🥉 | ✅ | ⚠️ | ✅ | ✅ | ✅ | ? | ✅ |
1038+
| [L][criteria-l] 🥉 | ✅ | ✅ | ⚠️ | ⚠️ | ✅ | ? | ✅ |
1039+
| [M][criteria-m] 🥈 | 🚫 | 🚫 | 🚫 | 🚫 | ✅ | ? | 🚫 |
1040+
| [N][criteria-n] 🥈 | ✅⚠️ | ✅⚠️ | ✅⚠️ | ✅⚠️ | ✅ | ? | ✅⚠️ |
1041+
| [O][criteria-o] 🥈 | ✅️ | 🚫️ | ❔ | 🚫 | ✅ | ? | ✅ |
9151042

9161043
[criteria-a]: #-a-graphql-should-contain-a-polymorphic-input-type
9171044
[criteria-b]: #-b-input-polymorphism-matches-output-polymorphism
@@ -934,12 +1061,17 @@ A quick glance at the evaluation results. Remember that passing or failing a spe
9341061
[solution-3]: #-3-order-based-discrimination
9351062
[solution-4]: #-4-structural-uniqueness
9361063
[solution-5]: #-5-one-of-tagged-union
1064+
[solution-6]: #-6-pending
1065+
[solution-7]: #-7-tagged-type
9371066

9381067
# ☑️ Decision Time!
9391068

940-
According to a simple [weight ranking](https://docs.google.com/spreadsheets/d/1ymKqI6BSTHGGHkf9IDjo0EJmOqMryS-uQRwDV77OF5g/edit?usp=sharing), here are the solutions in order:
1069+
Following meetings of the GraphQL Input Unions working group, [Solution 7][solution-7] was
1070+
proposed as an evolution of Solution 5, and is currently the leading solution.
1071+
1072+
~~According to a simple [weight ranking](https://docs.google.com/spreadsheets/d/1ymKqI6BSTHGGHkf9IDjo0EJmOqMryS-uQRwDV77OF5g/edit?usp=sharing), here are the solutions in order:~~
9411073

942-
* [5][solution-5]
943-
* [1][solution-1]
944-
* [2][solution-2]
945-
* [3][solution-3] / [4][solution-4]
1074+
* ~~[5][solution-5]~~
1075+
* ~~[1][solution-1]~~
1076+
* ~~[2][solution-2]~~
1077+
* ~~[3][solution-3] / [4][solution-4]~~

0 commit comments

Comments
 (0)