Repository: Khan/genqlient
Pull request #70 (2021-08-28 01:02:12)

If requested, validate binding-types get the right fields
## Summary:
One sharp edge of the new `bindings` setting (when used for composite
types) is this: the (presumably struct) type to which you're binding
may expect to have particular fields, but it's GraphQL so you could have
requested some other set of fields.  Now, if you ask us, we check.

Specifically, I've added a new setting under the `bindings` items, which
says: everywhere we query this must select these fields.  (Or use its
own inline `# @genqlient(bind: ...)`.)  It must select exactly those
fields, in order, no more, no less.  This was fairly easy to implement;
actually comparing the selections was surprisingly much code but it's
all pretty straightforward.

## Test plan:
make check



Patch:
diff --git a/DESIGN.md b/DESIGN.md
index 84d7ce7..c7c2a52 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -110,13 +110,13 @@ We'll do something similar to Apollo's naming scheme.  Specifically:
 
 All of this may be configurable later.
 
-### How to support fragments and interfaces
+### How to represent interfaces
 
 Consider the following query (suppose that `a` returns interface type `I`, which may be implemented by either `T` or `U`):
 
 ```graphql
-query { a { __typename b ...f } }
-fragment f on T { c d }
+query { a { __typename b ...F } }
+fragment F on T { c d }
 ```
 
 Depending on whether the concrete type returned from `a` is `T`, we can get one of two result structures back:
@@ -128,7 +128,7 @@ Depending on whether the concrete type returned from `a` is `T`, we can get one
 
 The question is: how do we translate that to Go types?
 
-One natural option is to generate a Go type for every concrete GraphQL type the object might have, and simply inline all the fragments.  So here we would have
+**Go interfaces:** one natural option is to generate a Go type for every concrete GraphQL type the object might have, and simply inline or embed all the fragments.  So here we would have
 ```go
 type T struct{ B, C, D string }
 type U struct{ B string }
@@ -147,13 +147,18 @@ Response{A: T{B: "...", C: "...", D: "..."}}
 Response{A: U{B: "..."}}
 ```
 
-Optionally, we might define a getter-method `GetB() string` on `T` and `U`, and include it in `I`, so that if you only need `B` you don't need to type-switch.  We could also define methods `GetC() string` and `GetD() string`, and another interface
+We can also define a getter-method `GetB() string` on `T` and `U`, and include it in `I`, so that if you only need `B` you don't need to type-switch.  Or, if that's not enough, we can also define a common embedded struct corresponding to the interface, so you can extract and pass that around if you want:
+
 ```go
-type F interface { isF(); GetC() string; GetD() string }
+type IEmbed struct { B string }
+type T struct { IEmbed; C, D string }
+type U struct { IEmbed }
+type I interface { isI(); GetIEmbed() IEmbed }
 ```
-so that if you want to use the same fragment `f` to share code in several places, you can do that, because all of the relevant types will implement `F`.  To do that we'll have to have the type-names for fragment fields rooted at the fragment rather than at the query, which is probably preferable anyway since they are guaranteed to be the same for all spreads of the fragment.  (Or we can just do the methods, and let you define the interface, although you still have to root the type-names for the same reason.) 
 
-Another natural option, which looks more like the way `shurcooL/graphql` does things, is to generate a type for each fragment, and only fill in the relevant ones:
+Note that this option gives a few different ways to represent fragments specifically, discussed in the next section.
+
+**Fragment fields:** another natural option, which looks more like the way `shurcooL/graphql` does things, is to generate a type for each fragment, and only fill in the relevant ones:
 ```go
 type Response struct {
     A struct {
@@ -183,13 +188,13 @@ query { a { b } }
 using the same schema as above.  In the former approach, we still define three types (plus two receivers); and `resp.A` is still of an interface type; it might be either `T` or `U`.  In the latter approach, this looks just like any other query: `resp.A` is a `struct{ B string }`.  This has implications for how we use this data: the latter approach lets us just do `resp.A.B`, whereas the former requires we do a type-switch, or add a `GetB()` method to `I`, and do `resp.A.GetB()`.
 
 
-Pros of the first approach:
+**Pros of Go interfaces:**
 
 - it's the most natural translation of how GraphQL does things, and provides the clearest guarantees about what is set when, what is mutually exclusive, etc.
 - you always know what type you got back
 - you always know which fields are there -- you don't have to encode at the application level an assumption that if fragment A was defined, fragment B also will be, because all types that match A also match B
 
-Pros of the second approach:
+**Pros of fragment fields:**
 
 - the types are simpler, and allow the option of unnamed types
 - if you query an interface, but don't care about the types, just the shared fields, you don't even have to think about any of this stuff
@@ -197,7 +202,7 @@ Pros of the second approach:
 
 Note that both approaches require that we add `__typename` to every selection set which has fragments (unless the types match exactly).  This seems fine since Apollo client also does so for all selection sets.  We also need to define `UnmarshalJSON` on every type with fragment-spreads; in the former case Go doesn't know how to unmarshal into an interface type, while in the latter the Go type structure is too different from that of the JSON.  (Note that `shurcooL/graphql` actually has its own JSON-decoder to solve this problem.)
 
-A third non-approach is to simplify define all the fields on the same struct, with some optional:
+**Flatten everything:** a third non-approach is to simplify define all the fields on the same struct, with some optional:
 
 ```go
 type Response struct {
@@ -211,7 +216,7 @@ type Response struct {
 
 Apart from being semantically messy, this doesn't have a good way to handle the case where there are types with conflicting fields, e.g.
 
-```
+```graphql
 interface I {}
 type T implements I { f: Int }
 type U implements I { f: String }
@@ -226,15 +231,157 @@ query {
 
 What type is `resp.A.F`?  It has to be both `string` and `int`.
 
-In other libraries:
-- Apollo does basically option 1, except with TypeScript/Flow's better support for sum types.
-- GraphQL Code Generator does basically option 1 (except with sum types instead of interfaces), except with unnamed types.  It definitely generates some ugly types, even with TypeScript/Flow's better support for sum types!
-- Khan's mobile autogen basically does option 1 (again with unnamed types, and sum types).
+**In other libraries:**
+- Apollo does basically the interface approach, except with TypeScript/Flow's better support for sum types.
+- GraphQL Code Generator does basically the interface approach (except with sum types instead of interfaces), except with unnamed types.  It definitely generates some ugly types, even with TypeScript/Flow's better support for sum types!
+- Khan's mobile autogen basically does the interface approach (again with unnamed types, and sum types).
 - gqlgen doesn't have this problem; on the server, fragments and interfaces are handled entirely in the framework and need not even be visible in user-land.
-- shurcooL/graphql uses option 2.
-- protobuf has a similar problem, and uses basically option 1 in Go (even though in other languages it uses something more like option 3).
+- shurcooL/graphql uses fragment fields.
+- protobuf has a similar problem, and uses basically the interface approach in Go (even though in other languages it uses something more like flattening everything).
+
+**Decision:** In general, it seems like the GraphQL Way, and perhaps also the Go Way is to use Go interfaces; I've always found the way shurcooL/graphql handles this to be a bit strange.  So I think it has to be at least the default.  In principle we could allow both though, since fragment fields are legitimately convenient for some cases, especially if you are querying shared fields of interfaces or using fragments as a code-sharing mechanism, since using Go interfaces handles those only through getter methods (although see below).
+
+### How to support fragments
+
+The previous section leaves some parts of how we'll handle fragments ambiguous.  Even within the way it lays things out, we generally have two options for how to represent a fragment.  Consider a query
+
+```graphql
+query MyQuery { a { b ...F } }
+fragment F on T { c d }
+```
+
+We'll have some struct (potentially several structs, one per implementation) representing the type of the value of `a`.  We can handle the fragment one of two ways: we can either flatten it into that type, such that the code is equivalent to writing `query Q { a { b c d } }` (except that `c` and `d` might be included for only some implementations, if `a` returns an interface), or we can represent it as a separate type `F` and embed it in the relevant structs.  (Or it could be a named field of said structs, but this seems to have little benefit, and it's nice to be able to access fields without knowing what fragment they're on.)  When the spread itself is abstract, there are a few sub-cases of the embed option.
+
+To get more concrete, there are [four cases](https://spec.graphql.org/June2018/#sec-Fragment-spread-is-possible) depending on the types in question.  (In all cases below, the methods, and other potential implementations of the interfaces, are elided.  Note also that I'm not sure I got all the type-names right exactly as genqlient would, but they should match approximately.)
+
+**Object spread in object scope:** The simplest spread is when we have an object-typed fragment spread into an object-typed selection.  (The two object types must be the same.)  This is typically used as a code-sharing mechanism.
+
+```graphql
+type Query { a: A }
+type A { b: String, c: String, d: String }
+
+query MyQuery { a { b ...F } }
+fragment F on A { c d }
+```
+
+```go
+// flattened:
+type MyQueryA struct { B, C, D string }
+
+// embedded
+type MyQueryA struct { B string; F }
+type F struct { C, D string }
+```
+
+**Abstract spread in object scope:** We can also spread an interface-typed fragment into an object-typed selection, again as a code-sharing mechanism.  (The object must implement the interface.)
+
+```graphql
+type Query { a: A }
+type A implements I { b: String, c: String, d: String }
+interface I { c: String, d: String }
+
+query MyQuery { a { b ...F } }
+fragment F on I { c d }
+```
+
+```go
+// flattened:
+type MyQueryA struct { B, C, D string }
+
+// embedded:
+type MyQueryA struct { B string; FA }
+type F interface { isF(); GetC() string; GetD() string } // for code-sharing purposes
+type FA struct { C, D string } // implements F
+```
+
+**Object spread in abstract scope:** This is the most common spread, perhaps, where you spread an object-typed fragment into an interface-typed selection in order to request some fields defined on a particular implementation of the interface.  (Again the object must implement the interface.)
+
+```graphql
+type Query { a: I }
+type A implements I { b: String, c: String, d: String }
+type T implements I { b: String, u: String, v: String }
+interface I { b: String }
+
+query MyQuery { a { b ...F ...G } }
+fragment F on A { c d }
+fragment G on A { u v }
+```
+
+```go
+// flattened:
+type MyQueryAI interface { isMyQueryAI(); GetB() string }
+type MyQueryAIA struct { B, C, D string } // implements MyQueryAI
+type MyQueryAIT struct { B, U, V string } // implements MyQueryAI
+
+// embedded:
+type MyQueryAI interface { isMyQueryAI(); GetB() string }
+type MyQueryAIA struct { B string; F } // implements MyQueryAI
+type MyQueryAIT struct { B string; G } // implements MyQueryAI
+type F struct { C, D string }
+type G struct { U, V string }
+```
+
+**Abstract spread in abstract scope:** This is a sort of combination of the last two, where you spread one interface's fragment into another interface, and can be used for code-sharing and/or to conditionally request fields.  Perhaps surprisingly, this is legal any time the two interfaces share an implementation, and neither need implement the other; this means there are arguably four cases of spreading a fragment of type `I` into a scope of type `J`: `I = J` (similar to object-in-object), `I implements J` (similar to abstract-in-object), `J implements I` (similar to object-in-abstract), and none of the above (which is quite rare).
+
+```graphql
+type Query { a: I }
+type A implements I & J { b: String, c: String, d: String }
+type T implements I { b: String }
+type U implements J { c: String, d: String, v: String }
+interface I { b: String }
+interface J { c: String, d: String }
+
+query MyQuery { a { b ...F } }
+fragment F on J { c d }
+```
+
+```go
+// flattened:
+type MyQueryAI interface { isMyQueryAI(); GetB() string }
+type MyQueryAIA struct { B, C, D string } // implements MyQueryAI (and MyQueryAJ if generated)
+type MyQueryAIT struct { B }              // implements MyQueryAI
+
+// embedded:
+type MyQueryAI interface { isMyQueryAI(); GetB() string }
+type MyQueryAIA struct { B string; FA } // implements MyQueryAI
+type MyQueryAIT struct { B string } // implements MyQueryAI
+type F interface { isF(); GetC() string; GetD() string }
+type FA struct { C, D string } // implements F
+type FU struct { C, D string } // implements F (never used; might be omitted)
+// if I == J or I implements J, perhaps additionally:
+type MyQueryAI interface { isMyQueryAI(); GetB() string; F }
+```
+
+Note in this case a third non-approach is
+
+```go
+// does not work:
+type MyQueryAI interface { isMyQueryAI(); GetB() string }
+type MyQueryAIA struct { B string; F } // implements MyQueryAI
+type F struct { C, D string }
+```
+
+This doesn't work because the fragment F might itself embed other fragments of object type.
+
+**Inline and named fragments:** Another complication is that in each of these cases, the fragment might be inline (`... on T { ... }`) or defined (`...F` where `fragment F on T { ... }`).  The latter is useful for sharing code, whereas the former may be more preferable when we just want request fields from a particular type.  Note that inline fragments have no natural name of their own; in the embedding approach we'd need to generate one.  (This is extra awkward because there's no prohibition on having several inline fragments of the same type in the same selection, e.g. `{ ... on T { id }, ... on T { name } }`, so even adding the type-name doesn't produce a unique name.)
+
+**Pros of flattening:**
+
+- Makes for the simplest resulting types, by far; the fragment adds no further complexity to the types.
+- Especially a simplification when embedding abstract-typed fragments, since you don't have to go through an extra layer of interface when you already have a concrete type.
+- More efficient when multiple fragments spread into the same selection contain the same field: we need only store it once whereas embedding must copy it once for each fragment.  Also easier to use in the same case, since if you have both `val.FragmentOne.MyField` and `val.FragmentTwo.MyField`, you can't access either via `val.MyField`.  (Empirically in the Khan Academy codebase this appears to be quite rare.)
+- If you need to manually construct values of genqlient-generated types, flattening will be a lot easier, but I don't really recommend doing that.
+
+**Pros of embedding:**
+
+- Results in cleaner type-names, since each (named) fragment can act as its own "root" for naming purposes.  (In principle we could do this even when flattening, although it might read somewhat awkwardly.)
+- Much more usable for deduplication; if you spread the fragment in several places in your query you can write a function which accepts an `F`, and pass it any of the data.  (Again we might be able to get this benefit, awkwardly, when flattening by generating an interface corresponding to each fragment, assuming we are also rooting the type-names at the fragment.)
+- It's easier to tell what fields go where; the casework for abstract-in-abstract embeds gets somewhat complex.
+- Arguably the most philosophically faithful representation of the GraphQL types in Go.
+
+Note in principle we could apply some of those benefits 
 
-**Decision:** In general, it seems like the GraphQL Way, and perhaps also the Go Way is Option 1; I've always found the way shurcooL/graphql handles this to be a bit strange.  So I think it has to be at least the default.  In principle we could allow both though, since option 2 is legitimately convenient for some cases, especially if you are querying shared fields of interfaces or using fragments as a code-sharing mechanism, since option 1 handles those only through getter methods.
+**Decision:** There are pros and cons both ways here; in general it seems embedding is the most natural where your goal is deduplication, whereas flattening is best for inline fragments; for named fragments used only once there's maybe a slight benefit to flattening but it's not a big difference either way.  If we have to go with one or the other, probably flattening is better.  But I think the best thing, unless it turns out to be too much work to implement, is probably just to flatten inline fragments and embed named ones.  (In principle we could also have a flag flatten named fragments, if we find a need.)
 
 ## Configuration and runtime
 
diff --git a/generate/binding_validation.go b/generate/binding_validation.go
new file mode 100644
index 0000000..9f5f8e2
--- /dev/null
+++ b/generate/binding_validation.go
@@ -0,0 +1,111 @@
+package generate
+
+// This file is responsible for doing the validation for type-bindings, if they
+// are so configured (see TypeBinding).
+
+import (
+	"fmt"
+
+	"github.com/vektah/gqlparser/v2/ast"
+	"github.com/vektah/gqlparser/v2/parser"
+)
+
+// selectionsMatch recursively compares the two selection-sets, and returns an
+// error if they differ.
+//
+// It does not check arguments and directives, only field names, aliases,
+// order, and fragment-structure.  It does not recurse into named fragments, it
+// only checks that their names match.
+//
+// TODO(benkraft): Should we check arguments/directives?
+func selectionsMatch(
+	pos *ast.Position,
+	expectedSelectionSet, actualSelectionSet ast.SelectionSet,
+) error {
+	if len(expectedSelectionSet) != len(actualSelectionSet) {
+		return errorf(
+			pos, "expected %d fields, got %d",
+			len(expectedSelectionSet), len(actualSelectionSet))
+	}
+
+	for i, expected := range expectedSelectionSet {
+		switch expected := expected.(type) {
+		case *ast.Field:
+			actual, ok := actualSelectionSet[i].(*ast.Field)
+			switch {
+			case !ok:
+				return errorf(actual.Position,
+					"expected selection #%d to be field, got %T",
+					i, actualSelectionSet[i])
+			case actual.Name != expected.Name:
+				return errorf(actual.Position,
+					"expected field %d to be %s, got %s",
+					i, expected.Name, actual.Name)
+			case actual.Alias != expected.Alias:
+				return errorf(actual.Position,
+					"expected field %d's alias to be %s, got %s",
+					i, expected.Alias, actual.Alias)
+			}
+			err := selectionsMatch(actual.Position, expected.SelectionSet, actual.SelectionSet)
+			if err != nil {
+				return fmt.Errorf("in %s sub-selection: %w", actual.Alias, err)
+			}
+		case *ast.InlineFragment:
+			actual, ok := actualSelectionSet[i].(*ast.InlineFragment)
+			switch {
+			case !ok:
+				return errorf(actual.Position,
+					"expected selection %d to be inline fragment, got %T",
+					i, actualSelectionSet[i])
+			case actual.TypeCondition != expected.TypeCondition:
+				return errorf(actual.Position,
+					"expected fragment %d to be on type %s, got %s",
+					i, expected.TypeCondition, actual.TypeCondition)
+			}
+			err := selectionsMatch(actual.Position, expected.SelectionSet, actual.SelectionSet)
+			if err != nil {
+				return fmt.Errorf("in inline fragment on %s: %w", actual.TypeCondition, err)
+			}
+		case *ast.FragmentSpread:
+			actual, ok := actualSelectionSet[i].(*ast.FragmentSpread)
+			switch {
+			case !ok:
+				return errorf(actual.Position,
+					"expected selection %d to be fragment spread, got %T",
+					i, actualSelectionSet[i])
+			case actual.Name != expected.Name:
+				return errorf(actual.Position,
+					"expected fragment %d to be ...%s, got ...%s",
+					i, expected.Name, actual.Name)
+			}
+		}
+	}
+	return nil
+}
+
+// validateBindingSelection checks that if you requested in your type-binding
+// that this type must always request certain fields, then in fact it does.
+func (g *generator) validateBindingSelection(
+	typeName string,
+	binding *TypeBinding,
+	pos *ast.Position,
+	selectionSet ast.SelectionSet,
+) error {
+	if binding.ExpectExactFields == "" {
+		return nil // no validation requested
+	}
+
+	// HACK: we parse the selection as if it were a query, which is basically
+	// the same (for syntax purposes; it of course wouldn't validate)
+	doc, gqlErr := parser.ParseQuery(&ast.Source{Input: binding.ExpectExactFields})
+	if gqlErr != nil {
+		return errorf(
+			nil, "invalid type-binding %s.expect_exact_fields: %w", typeName, gqlErr)
+	}
+
+	err := selectionsMatch(pos, doc.Operations[0].SelectionSet, selectionSet)
+	if err != nil {
+		return fmt.Errorf("invalid selection for type-binding %s: %w", typeName, err)
+	}
+	return nil
+}
diff --git a/generate/config.go b/generate/config.go
index 7ea3fcf..08c2a0c 100644
--- a/generate/config.go
+++ b/generate/config.go
@@ -104,6 +104,26 @@ type TypeBinding struct {
 	//  map[string]interface{}
 	//	github.com/you/yourpkg/subpkg.MyType
 	Type string `yaml:"type"`
+	// If set, a GraphQL selection which must exactly match the fields
+	// requested whenever this type is used.  Only applies if the GraphQL type
+	// is a composite output type (object, interface, or union).
+	//
+	// This is useful if Type is a struct whose UnmarshalJSON or other methods
+	// expect that you requested certain fields.  You can specify those fields
+	// like
+	//  MyType:
+	//		type: path/to/my.GoType
+	//		expect_exact_fields: "{ id name }"
+	// and then genqlient will reject if you make a query
+	//	{ fieldOfMytype { id title } }
+	// The fields must match exactly, including the ordering: "{ name id }"
+	// will be rejected.  But the arguments and directives, if any, need not
+	// match.
+	//
+	// TODO(benkraft): Also add ExpectIncludesFields and ExpectSubsetOfFields,
+	// or something, if you want to say, for example, that you have to request
+	// certain fields but others are optional.
+	ExpectExactFields string `yaml:"expect_exact_fields"`
 }
 
 // baseDir returns the directory of the config-file (relative to which
diff --git a/generate/convert.go b/generate/convert.go
index d506615..d1531b8 100644
--- a/generate/convert.go
+++ b/generate/convert.go
@@ -190,6 +190,13 @@ func (g *generator) convertDefinition(
 	// unless the binding is "-" which means "ignore the global binding".
 	globalBinding, ok := g.Config.Bindings[def.Name]
 	if ok && options.Bind != "-" {
+		if def.Kind == ast.Object || def.Kind == ast.Interface || def.Kind == ast.Union {
+			err := g.validateBindingSelection(
+				name, globalBinding, pos, selectionSet)
+			if err != nil {
+				return nil, err
+			}
+		}
 		goRef, err := g.addRef(globalBinding.Type)
 		return &goOpaqueType{goRef}, err
 	}
@@ -200,38 +207,20 @@ func (g *generator) convertDefinition(
 
 	switch def.Kind {
 	case ast.Object:
+		fields, err := g.convertSelectionSet(
+			namePrefix, selectionSet, def, queryOptions)
+		if err != nil {
+			return nil, err
+		}
+
 		goType := &goStructType{
 			GoName:      name,
 			Description: def.Description,
 			GraphQLName: def.Name,
-			Fields:      make([]*goStructField, len(selectionSet)),
+			Fields:      fields,
 			Incomplete:  true,
 		}
 		g.typeMap[name] = goType
-
-		for i, selection := range selectionSet {
-			_, selectionDirective, err := g.parsePrecedingComment(
-				selection, selection.GetPosition())
-			if err != nil {
-				return nil, err
-			}
-			selectionOptions := queryOptions.merge(selectionDirective)
-
-			switch selection := selection.(type) {
-			case *ast.Field:
-				goType.Fields[i], err = g.convertField(
-					namePrefix, selection, selectionOptions, queryOptions)
-				if err != nil {
-					return nil, err
-				}
-			case *ast.FragmentSpread:
-				return nil, errorf(selection.Position, "not implemented: %T", selection)
-			case *ast.InlineFragment:
-				return nil, errorf(selection.Position, "not implemented: %T", selection)
-			default:
-				return nil, errorf(nil, "invalid selection type: %T", selection)
-			}
-		}
 		return goType, nil
 
 	case ast.InputObject:
@@ -270,38 +259,22 @@ func (g *generator) convertDefinition(
 		return goType, nil
 
 	case ast.Interface, ast.Union:
+		sharedFields, err := g.convertSelectionSet(
+			namePrefix, selectionSet, def, queryOptions)
+		if err != nil {
+			return nil, err
+		}
+
 		implementationTypes := g.schema.GetPossibleTypes(def)
 		goType := &goInterfaceType{
 			GoName:          name,
 			Description:     def.Description,
 			GraphQLName:     def.Name,
-			SharedFields:    make([]*goStructField, 0, len(selectionSet)),
+			SharedFields:    sharedFields,
 			Implementations: make([]*goStructType, len(implementationTypes)),
 		}
 		g.typeMap[name] = goType
 
-		// TODO(benkraft): This sorta-duplicates what we'll do in each
-		// implementation when it traverses the fields.  But they'll differ
-		// more once we support fragments; at that point we should figure out
-		// how to refactor.
-		for _, selection := range selectionSet {
-			field, ok := selection.(*ast.Field)
-			if !ok { // fragment/interface, not a shared field
-				continue
-			}
-			_, fieldDirective, err := g.parsePrecedingComment(field, field.GetPosition())
-			if err != nil {
-				return nil, err
-			}
-			fieldOptions := queryOptions.merge(fieldDirective)
-
-			goField, err := g.convertField(namePrefix, field, fieldOptions, queryOptions)
-			if err != nil {
-				return nil, err
-			}
-			goType.SharedFields = append(goType.SharedFields, goField)
-		}
-
 		for i, implDef := range implementationTypes {
 			// Note for shared fields we propagate forward the interface's
 			// name-prefix: that is, the implementations will have fields with
@@ -357,7 +330,157 @@ func (g *generator) convertDefinition(
 	}
 }
 
-// convertField converts a single GraphQL operation-field into a GraphQL type.
+// convertSelectionSet converts a GraphQL selection-set into a list of
+// corresponding Go struct-fields (and their Go types)
+//
+// A selection-set is a list of fields within braces like `{ myField }`, as
+// appears at the toplevel of a query, in a field's sub-selections, or within
+// an inline or named fragment.
+//
+// containingTypedef is the type-def whose fields we are selecting, and may be
+// an object type or an interface type.  In the case of interfaces, we'll call
+// convertSelectionSet once for the interface, and once for each
+// implementation.
+func (g *generator) convertSelectionSet(
+	namePrefix string,
+	selectionSet ast.SelectionSet,
+	containingTypedef *ast.Definition,
+	queryOptions *GenqlientDirective,
+) ([]*goStructField, error) {
+	fields := make([]*goStructField, 0, len(selectionSet))
+	for _, selection := range selectionSet {
+		_, selectionDirective, err := g.parsePrecedingComment(
+			selection, selection.GetPosition())
+		if err != nil {
+			return nil, err
+		}
+		selectionOptions := queryOptions.merge(selectionDirective)
+
+		switch selection := selection.(type) {
+		case *ast.Field:
+			field, err := g.convertField(
+				namePrefix, selection, selectionOptions, queryOptions)
+			if err != nil {
+				return nil, err
+			}
+			fields = append(fields, field)
+		case *ast.FragmentSpread:
+			return nil, errorf(selection.Position, "not implemented: %T", selection)
+		case *ast.InlineFragment:
+			// (Note this will return nil, nil if the fragment doesn't apply to
+			// this type.)
+			fragmentFields, err := g.convertInlineFragment(
+				namePrefix, selection, containingTypedef, queryOptions)
+			if err != nil {
+				return nil, err
+			}
+			fields = append(fields, fragmentFields...)
+		default:
+			return nil, errorf(nil, "invalid selection type: %T", selection)
+		}
+	}
+
+	// We need to deduplicate, if you asked for
+	//	{ id, id, id, ... on SubType { id } }
+	// (which, yes, is legal) we'll treat that as just { id }.
+	uniqFields := make([]*goStructField, 0, len(selectionSet))
+	fieldNames := make(map[string]bool, len(selectionSet))
+	for _, field := range fields {
+		// GraphQL (and, effectively, JSON) requires that all fields with the
+		// same alias (JSON-name) must be the same (i.e. refer to the same
+		// field), so that's how we deduplicate.
+		if fieldNames[field.JSONName] {
+			// GraphQL (and, effectively, JSON) forbids you from having two
+			// fields with the same alias (JSON-name) that refer to different
+			// GraphQL fields.  But it does allow you to have the same field
+			// with different selections (subject to some additional rules).
+			// We say: that's too complicated! and allow duplicate fields
+			// only if they're "leaf" types (enum or scalar).
+			switch field.GoType.Unwrap().(type) {
+			case *goOpaqueType, *goEnumType:
+				// Leaf field; we can just deduplicate.
+				// Note GraphQL already guarantees that the conflicting field
+				// has scalar/enum type iff this field does:
+				// https://spec.graphql.org/draft/#SameResponseShape()
+				continue
+			case *goStructType, *goInterfaceType:
+				// TODO(benkraft): Keep track of the position of each
+				// selection, so we can put this error on the right line.
+				return nil, errorf(nil,
+					"genqlient doesn't allow duplicate fields with different selections "+
+						"(see https://github.com/Khan/genqlient/issues/64); "+
+						"duplicate field: %s.%s", containingTypedef.Name, field.JSONName)
+			default:
+				return nil, errorf(nil, "unexpected field-type: %T", field.GoType.Unwrap())
+			}
+		}
+		uniqFields = append(uniqFields, field)
+		fieldNames[field.JSONName] = true
+	}
+	return uniqFields, nil
+}
+
+// fragmentMatches returns true if the given fragment is "active" when applied
+// to the given type.
+//
+// "Active" here means "the fragment's fields will be returned on all objects
+// of the given type", which is true when the given type is or implements
+// the fragment's type.  This is distinct from the rules for when a fragment
+// spread is legal, which is true when the fragment would be active for *any*
+// of the concrete types the spread-context could have (see
+// https://spec.graphql.org/draft/#sec-Fragment-Spreads or DESIGN.md).
+//
+// containingTypedef is as described in convertInlineFragment, below.
+// fragmentTypedef is the definition of the fragment's type-condition, i.e. the
+// definition of MyType in a fragment `on MyType`.
+func fragmentMatches(containingTypedef, fragmentTypedef *ast.Definition) bool {
+	if containingTypedef.Name == fragmentTypedef.Name {
+		return true
+	}
+	for _, iface := range containingTypedef.Interfaces {
+		// Note we don't need to recurse into the interfaces here, because in
+		// GraphQL types must list all the interfaces they implement, including
+		// all types those interfaces implement [1].  Actually, at present
+		// gqlparser doesn't even support interfaces implementing other
+		// interfaces, but our code would handle that too.
+		// [1] https://spec.graphql.org/draft/#sec-Interfaces.Interfaces-Implementing-Interfaces
+		if iface == fragmentTypedef.Name {
+			return true
+		}
+	}
+	return false
+}
+
+// convertInlineFragment converts a single GraphQL inline fragment
+// (`... on MyType { myField }`) into Go struct-fields.
+//
+// containingTypedef is the type-def corresponding to the type into which we
+// are spreading; it may be either an interface type (when spreading into one)
+// or an object type (when writing the implementations of such an interface, or
+// when using an inline fragment in an object type which is rare).  If the
+// given fragment does not apply to that type, this function returns nil, nil.
+//
+// In general, we treat such fragments' fields as if they were fields of the
+// parent selection-set (except of course they are only included in types the
+// fragment matches); see DESIGN.md for more.
+func (g *generator) convertInlineFragment(
+	namePrefix string,
+	fragment *ast.InlineFragment,
+	containingTypedef *ast.Definition,
+	queryOptions *GenqlientDirective,
+) ([]*goStructField, error) {
+	// You might think fragmentTypedef would be fragment.ObjectDefinition, but
+	// actually that's the type into which the fragment is spread.
+	fragmentTypedef := g.schema.Types[fragment.TypeCondition]
+	if !fragmentMatches(containingTypedef, fragmentTypedef) {
+		return nil, nil
+	}
+	return g.convertSelectionSet(namePrefix, fragment.SelectionSet,
+		containingTypedef, queryOptions)
+}
+
+// convertField converts a single GraphQL operation-field into a Go
+// struct-field (and its type).
 //
 // Note that input-type fields are handled separately (inline in
 // convertDefinition), because they come from the type-definition, not the
diff --git a/generate/unmarshal.go.tmpl b/generate/unmarshal.go.tmpl
index 980d3b6..d91a7f6 100644
--- a/generate/unmarshal.go.tmpl
+++ b/generate/unmarshal.go.tmpl
@@ -85,7 +85,8 @@ func (v *{{.GoName}}) UnmarshalJSON(b []byte) error {
         err = __unmarshal{{$field.GoType.Unwrap.Reference}}(
             {{if $field.GoType.IsPointer}}*{{end}}target, raw)
         if err != nil {
-            return err
+            return fmt.Errorf(
+                "Unable to unmarshal {{$.GoName}}.{{$field.GoName}}: %w", err)
         }
         {{range $i := intRange $field.GoType.SliceDepth -}}
         }
diff --git a/generate/unmarshal_helper.go.tmpl b/generate/unmarshal_helper.go.tmpl
index 8916375..a2b721f 100644
--- a/generate/unmarshal_helper.go.tmpl
+++ b/generate/unmarshal_helper.go.tmpl
@@ -17,10 +17,15 @@ func __unmarshal{{.GoName}}(v *{{.GoName}}, m {{ref "encoding/json.RawMessage"}}
     switch tn.TypeName {
     {{range .Implementations -}}
     case "{{.GraphQLName}}":
-        {{/* TODO: handle repeated fields! */ -}}
         *v = new({{.GoName}})
         return {{ref "encoding/json.Unmarshal"}}(m, *v)
     {{end -}}
+    case "":
+        {{/* Likely if we're making a request to a mock server and the author
+             of the mock didn't know to add __typename, so give a special
+             error. */ -}}
+        return {{ref "fmt.Errorf"}}(
+            "Response was missing {{.GraphQLName}}.__typename")
     default:
         return {{ref "fmt.Errorf"}}(
             `Unexpected concrete type for {{.GoName}}: "%v"`, tn.TypeName)
diff --git a/internal/integration/generated.go b/internal/integration/generated.go
index 69e7250..c89881e 100644
--- a/internal/integration/generated.go
+++ b/internal/integration/generated.go
@@ -10,6 +10,261 @@ import (
 	"github.com/Khan/genqlient/graphql"
 )
 
+type Species string
+
+const (
+	SpeciesDog        Species = "DOG"
+	SpeciesCoelacanth Species = "COELACANTH"
+)
+
+// queryWithFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal.
+type queryWithFragmentsBeingsAnimal struct {
+	Typename string                             `json:"__typename"`
+	Id       string                             `json:"id"`
+	Name     string                             `json:"name"`
+	Hair     queryWithFragmentsBeingsHair       `json:"hair"`
+	Species  Species                            `json:"species"`
+	Owner    queryWithFragmentsBeingsOwnerBeing `json:"-"`
+}
+
+func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
+
+	type queryWithFragmentsBeingsAnimalWrapper queryWithFragmentsBeingsAnimal
+
+	var firstPass struct {
+		*queryWithFragmentsBeingsAnimalWrapper
+		Owner json.RawMessage `json:"owner"`
+	}
+	firstPass.queryWithFragmentsBeingsAnimalWrapper = (*queryWithFragmentsBeingsAnimalWrapper)(v)
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.Owner
+		raw := firstPass.Owner
+		err = __unmarshalqueryWithFragmentsBeingsOwnerBeing(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal queryWithFragmentsBeingsAnimal.Owner: %w", err)
+		}
+	}
+	return nil
+}
+
+// queryWithFragmentsBeingsBeing includes the requested fields of the GraphQL interface Being.
+//
+// queryWithFragmentsBeingsBeing is implemented by the following types:
+// queryWithFragmentsBeingsUser
+// queryWithFragmentsBeingsAnimal
+//
+// The GraphQL type's documentation follows.
+//
+//
+type queryWithFragmentsBeingsBeing interface {
+	implementsGraphQLInterfacequeryWithFragmentsBeingsBeing()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+	// GetId returns the interface-field "id" from its implementation.
+	GetId() string
+	// GetName returns the interface-field "name" from its implementation.
+	GetName() string
+}
+
+func (v *queryWithFragmentsBeingsUser) implementsGraphQLInterfacequeryWithFragmentsBeingsBeing() {}
+
+// GetTypename is a part of, and documented with, the interface queryWithFragmentsBeingsBeing.
+func (v *queryWithFragmentsBeingsUser) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface queryWithFragmentsBeingsBeing.
+func (v *queryWithFragmentsBeingsUser) GetId() string { return v.Id }
+
+// GetName is a part of, and documented with, the interface queryWithFragmentsBeingsBeing.
+func (v *queryWithFragmentsBeingsUser) GetName() string { return v.Name }
+
+func (v *queryWithFragmentsBeingsAnimal) implementsGraphQLInterfacequeryWithFragmentsBeingsBeing() {}
+
+// GetTypename is a part of, and documented with, the interface queryWithFragmentsBeingsBeing.
+func (v *queryWithFragmentsBeingsAnimal) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface queryWithFragmentsBeingsBeing.
+func (v *queryWithFragmentsBeingsAnimal) GetId() string { return v.Id }
+
+// GetName is a part of, and documented with, the interface queryWithFragmentsBeingsBeing.
+func (v *queryWithFragmentsBeingsAnimal) GetName() string { return v.Name }
+
+func __unmarshalqueryWithFragmentsBeingsBeing(v *queryWithFragmentsBeingsBeing, m json.RawMessage) error {
+	if string(m) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(m, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "User":
+		*v = new(queryWithFragmentsBeingsUser)
+		return json.Unmarshal(m, *v)
+	case "Animal":
+		*v = new(queryWithFragmentsBeingsAnimal)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Being.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for queryWithFragmentsBeingsBeing: "%v"`, tn.TypeName)
+	}
+}
+
+// queryWithFragmentsBeingsHair includes the requested fields of the GraphQL type BeingsHair.
+type queryWithFragmentsBeingsHair struct {
+	HasHair bool `json:"hasHair"`
+}
+
+// queryWithFragmentsBeingsOwnerAnimal includes the requested fields of the GraphQL type Animal.
+type queryWithFragmentsBeingsOwnerAnimal struct {
+	Typename string `json:"__typename"`
+	Id       string `json:"id"`
+	Name     string `json:"name"`
+}
+
+// queryWithFragmentsBeingsOwnerBeing includes the requested fields of the GraphQL interface Being.
+//
+// queryWithFragmentsBeingsOwnerBeing is implemented by the following types:
+// queryWithFragmentsBeingsOwnerUser
+// queryWithFragmentsBeingsOwnerAnimal
+//
+// The GraphQL type's documentation follows.
+//
+//
+type queryWithFragmentsBeingsOwnerBeing interface {
+	implementsGraphQLInterfacequeryWithFragmentsBeingsOwnerBeing()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+	// GetId returns the interface-field "id" from its implementation.
+	GetId() string
+	// GetName returns the interface-field "name" from its implementation.
+	GetName() string
+}
+
+func (v *queryWithFragmentsBeingsOwnerUser) implementsGraphQLInterfacequeryWithFragmentsBeingsOwnerBeing() {
+}
+
+// GetTypename is a part of, and documented with, the interface queryWithFragmentsBeingsOwnerBeing.
+func (v *queryWithFragmentsBeingsOwnerUser) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface queryWithFragmentsBeingsOwnerBeing.
+func (v *queryWithFragmentsBeingsOwnerUser) GetId() string { return v.Id }
+
+// GetName is a part of, and documented with, the interface queryWithFragmentsBeingsOwnerBeing.
+func (v *queryWithFragmentsBeingsOwnerUser) GetName() string { return v.Name }
+
+func (v *queryWithFragmentsBeingsOwnerAnimal) implementsGraphQLInterfacequeryWithFragmentsBeingsOwnerBeing() {
+}
+
+// GetTypename is a part of, and documented with, the interface queryWithFragmentsBeingsOwnerBeing.
+func (v *queryWithFragmentsBeingsOwnerAnimal) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface queryWithFragmentsBeingsOwnerBeing.
+func (v *queryWithFragmentsBeingsOwnerAnimal) GetId() string { return v.Id }
+
+// GetName is a part of, and documented with, the interface queryWithFragmentsBeingsOwnerBeing.
+func (v *queryWithFragmentsBeingsOwnerAnimal) GetName() string { return v.Name }
+
+func __unmarshalqueryWithFragmentsBeingsOwnerBeing(v *queryWithFragmentsBeingsOwnerBeing, m json.RawMessage) error {
+	if string(m) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(m, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "User":
+		*v = new(queryWithFragmentsBeingsOwnerUser)
+		return json.Unmarshal(m, *v)
+	case "Animal":
+		*v = new(queryWithFragmentsBeingsOwnerAnimal)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Being.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for queryWithFragmentsBeingsOwnerBeing: "%v"`, tn.TypeName)
+	}
+}
+
+// queryWithFragmentsBeingsOwnerUser includes the requested fields of the GraphQL type User.
+type queryWithFragmentsBeingsOwnerUser struct {
+	Typename    string `json:"__typename"`
+	Id          string `json:"id"`
+	Name        string `json:"name"`
+	LuckyNumber int    `json:"luckyNumber"`
+}
+
+// queryWithFragmentsBeingsUser includes the requested fields of the GraphQL type User.
+type queryWithFragmentsBeingsUser struct {
+	Typename    string                       `json:"__typename"`
+	Id          string                       `json:"id"`
+	Name        string                       `json:"name"`
+	LuckyNumber int                          `json:"luckyNumber"`
+	Hair        queryWithFragmentsBeingsHair `json:"hair"`
+}
+
+// queryWithFragmentsResponse is returned by queryWithFragments on success.
+type queryWithFragmentsResponse struct {
+	Beings []queryWithFragmentsBeingsBeing `json:"-"`
+}
+
+func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error {
+
+	type queryWithFragmentsResponseWrapper queryWithFragmentsResponse
+
+	var firstPass struct {
+		*queryWithFragmentsResponseWrapper
+		Beings []json.RawMessage `json:"beings"`
+	}
+	firstPass.queryWithFragmentsResponseWrapper = (*queryWithFragmentsResponseWrapper)(v)
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.Beings
+		raw := firstPass.Beings
+		*target = make(
+			[]queryWithFragmentsBeingsBeing,
+			len(raw))
+		for i, raw := range raw {
+			target := &(*target)[i]
+			err = __unmarshalqueryWithFragmentsBeingsBeing(
+				target, raw)
+			if err != nil {
+				return fmt.Errorf(
+					"Unable to unmarshal queryWithFragmentsResponse.Beings: %w", err)
+			}
+		}
+	}
+	return nil
+}
+
 // queryWithInterfaceListFieldBeingsAnimal includes the requested fields of the GraphQL type Animal.
 type queryWithInterfaceListFieldBeingsAnimal struct {
 	Typename string `json:"__typename"`
@@ -80,6 +335,9 @@ func __unmarshalqueryWithInterfaceListFieldBeingsBeing(v *queryWithInterfaceList
 	case "Animal":
 		*v = new(queryWithInterfaceListFieldBeingsAnimal)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Being.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for queryWithInterfaceListFieldBeingsBeing: "%v"`, tn.TypeName)
@@ -124,7 +382,8 @@ func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error {
 			err = __unmarshalqueryWithInterfaceListFieldBeingsBeing(
 				target, raw)
 			if err != nil {
-				return err
+				return fmt.Errorf(
+					"Unable to unmarshal queryWithInterfaceListFieldResponse.Beings: %w", err)
 			}
 		}
 	}
@@ -201,6 +460,9 @@ func __unmarshalqueryWithInterfaceListPointerFieldBeingsBeing(v *queryWithInterf
 	case "Animal":
 		*v = new(queryWithInterfaceListPointerFieldBeingsAnimal)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Being.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for queryWithInterfaceListPointerFieldBeingsBeing: "%v"`, tn.TypeName)
@@ -246,7 +508,8 @@ func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) err
 			err = __unmarshalqueryWithInterfaceListPointerFieldBeingsBeing(
 				*target, raw)
 			if err != nil {
-				return err
+				return fmt.Errorf(
+					"Unable to unmarshal queryWithInterfaceListPointerFieldResponse.Beings: %w", err)
 			}
 		}
 	}
@@ -316,6 +579,9 @@ func __unmarshalqueryWithInterfaceNoFragmentsBeing(v *queryWithInterfaceNoFragme
 	case "Animal":
 		*v = new(queryWithInterfaceNoFragmentsBeingAnimal)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Being.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for queryWithInterfaceNoFragmentsBeing: "%v"`, tn.TypeName)
@@ -369,7 +635,8 @@ func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error {
 		err = __unmarshalqueryWithInterfaceNoFragmentsBeing(
 			target, raw)
 		if err != nil {
-			return err
+			return fmt.Errorf(
+				"Unable to unmarshal queryWithInterfaceNoFragmentsResponse.Being: %w", err)
 		}
 	}
 	return nil
@@ -537,3 +804,59 @@ query queryWithInterfaceListPointerField ($ids: [ID!]!) {
 	)
 	return &retval, err
 }
+
+func queryWithFragments(
+	ctx context.Context,
+	client graphql.Client,
+	ids []string,
+) (*queryWithFragmentsResponse, error) {
+	variables := map[string]interface{}{
+		"ids": ids,
+	}
+
+	var retval queryWithFragmentsResponse
+	err := client.MakeRequest(
+		ctx,
+		"queryWithFragments",
+		`
+query queryWithFragments ($ids: [ID!]!) {
+	beings(ids: $ids) {
+		__typename
+		id
+		... on Being {
+			id
+			name
+		}
+		... on Animal {
+			id
+			hair {
+				hasHair
+			}
+			species
+			owner {
+				__typename
+				id
+				... on Being {
+					name
+				}
+				... on User {
+					luckyNumber
+				}
+			}
+		}
+		... on Lucky {
+			luckyNumber
+		}
+		... on User {
+			hair {
+				color
+			}
+		}
+	}
+}
+`,
+		&retval,
+		variables,
+	)
+	return &retval, err
+}
diff --git a/internal/integration/schema.graphql b/internal/integration/schema.graphql
index 5c42e46..de6d3f5 100644
--- a/internal/integration/schema.graphql
+++ b/internal/integration/schema.graphql
@@ -3,21 +3,28 @@ type Query {
   user(id: ID!): User
   being(id: ID!): Being
   beings(ids: [ID!]!): [Being]!
+  lotteryWinner(number: Int!): Lucky
 }
 
-type User implements Being {
+type User implements Being & Lucky {
   id: ID!
   name: String!
   luckyNumber: Int
+  hair: Hair
 }
 
+type Hair { color: String }   # silly name to confuse the name-generator
+
 type Animal implements Being {
   id: ID!
   name: String!
   species: Species!
   owner: Being
+  hair: BeingsHair
 }
 
+type BeingsHair { hasHair: Boolean! }   # silly name to confuse the name-generator
+
 enum Species {
   DOG
   COELACANTH
@@ -27,3 +34,7 @@ interface Being {
   id: ID!
   name: String!
 }
+
+interface Lucky {
+  luckyNumber: Int
+}
diff --git a/internal/integration/server/gqlgen_exec.go b/internal/integration/server/gqlgen_exec.go
index d50db5a..709824f 100644
--- a/internal/integration/server/gqlgen_exec.go
+++ b/internal/integration/server/gqlgen_exec.go
@@ -43,20 +43,31 @@ type DirectiveRoot struct {
 
 type ComplexityRoot struct {
 	Animal struct {
+		Hair    func(childComplexity int) int
 		ID      func(childComplexity int) int
 		Name    func(childComplexity int) int
 		Owner   func(childComplexity int) int
 		Species func(childComplexity int) int
 	}
 
+	BeingsHair struct {
+		HasHair func(childComplexity int) int
+	}
+
+	Hair struct {
+		Color func(childComplexity int) int
+	}
+
 	Query struct {
-		Being  func(childComplexity int, id string) int
-		Beings func(childComplexity int, ids []string) int
-		Me     func(childComplexity int) int
-		User   func(childComplexity int, id string) int
+		Being         func(childComplexity int, id string) int
+		Beings        func(childComplexity int, ids []string) int
+		LotteryWinner func(childComplexity int, number int) int
+		Me            func(childComplexity int) int
+		User          func(childComplexity int, id string) int
 	}
 
 	User struct {
+		Hair        func(childComplexity int) int
 		ID          func(childComplexity int) int
 		LuckyNumber func(childComplexity int) int
 		Name        func(childComplexity int) int
@@ -68,6 +79,7 @@ type QueryResolver interface {
 	User(ctx context.Context, id string) (*User, error)
 	Being(ctx context.Context, id string) (Being, error)
 	Beings(ctx context.Context, ids []string) ([]Being, error)
+	LotteryWinner(ctx context.Context, number int) (Lucky, error)
 }
 
 type executableSchema struct {
@@ -85,6 +97,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 	_ = ec
 	switch typeName + "." + field {
 
+	case "Animal.hair":
+		if e.complexity.Animal.Hair == nil {
+			break
+		}
+
+		return e.complexity.Animal.Hair(childComplexity), true
+
 	case "Animal.id":
 		if e.complexity.Animal.ID == nil {
 			break
@@ -113,6 +132,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Animal.Species(childComplexity), true
 
+	case "BeingsHair.hasHair":
+		if e.complexity.BeingsHair.HasHair == nil {
+			break
+		}
+
+		return e.complexity.BeingsHair.HasHair(childComplexity), true
+
+	case "Hair.color":
+		if e.complexity.Hair.Color == nil {
+			break
+		}
+
+		return e.complexity.Hair.Color(childComplexity), true
+
 	case "Query.being":
 		if e.complexity.Query.Being == nil {
 			break
@@ -137,6 +170,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Query.Beings(childComplexity, args["ids"].([]string)), true
 
+	case "Query.lotteryWinner":
+		if e.complexity.Query.LotteryWinner == nil {
+			break
+		}
+
+		args, err := ec.field_Query_lotteryWinner_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Query.LotteryWinner(childComplexity, args["number"].(int)), true
+
 	case "Query.me":
 		if e.complexity.Query.Me == nil {
 			break
@@ -156,6 +201,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Query.User(childComplexity, args["id"].(string)), true
 
+	case "User.hair":
+		if e.complexity.User.Hair == nil {
+			break
+		}
+
+		return e.complexity.User.Hair(childComplexity), true
+
 	case "User.id":
 		if e.complexity.User.ID == nil {
 			break
@@ -232,21 +284,28 @@ var sources = []*ast.Source{
   user(id: ID!): User
   being(id: ID!): Being
   beings(ids: [ID!]!): [Being]!
+  lotteryWinner(number: Int!): Lucky
 }
 
-type User implements Being {
+type User implements Being & Lucky {
   id: ID!
   name: String!
   luckyNumber: Int
+  hair: Hair
 }
 
+type Hair { color: String }   # silly name to confuse the name-generator
+
 type Animal implements Being {
   id: ID!
   name: String!
   species: Species!
   owner: Being
+  hair: BeingsHair
 }
 
+type BeingsHair { hasHair: Boolean! }   # silly name to confuse the name-generator
+
 enum Species {
   DOG
   COELACANTH
@@ -256,6 +315,10 @@ interface Being {
   id: ID!
   name: String!
 }
+
+interface Lucky {
+  luckyNumber: Int
+}
 `, BuiltIn: false},
 }
 var parsedSchema = gqlparser.MustLoadSchema(sources...)
@@ -309,6 +372,21 @@ func (ec *executionContext) field_Query_beings_args(ctx context.Context, rawArgs
 	return args, nil
 }
 
+func (ec *executionContext) field_Query_lotteryWinner_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 int
+	if tmp, ok := rawArgs["number"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("number"))
+		arg0, err = ec.unmarshalNInt2int(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["number"] = arg0
+	return args, nil
+}
+
 func (ec *executionContext) field_Query_user_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -499,6 +577,105 @@ func (ec *executionContext) _Animal_owner(ctx context.Context, field graphql.Col
 	return ec.marshalOBeing2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeing(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _Animal_hair(ctx context.Context, field graphql.CollectedField, obj *Animal) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "Animal",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   false,
+		IsResolver: false,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Hair, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*BeingsHair)
+	fc.Result = res
+	return ec.marshalOBeingsHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeingsHair(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _BeingsHair_hasHair(ctx context.Context, field graphql.CollectedField, obj *BeingsHair) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "BeingsHair",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   false,
+		IsResolver: false,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.HasHair, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.(bool)
+	fc.Result = res
+	return ec.marshalNBoolean2bool(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _Hair_color(ctx context.Context, field graphql.CollectedField, obj *Hair) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "Hair",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   false,
+		IsResolver: false,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Color, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalOString2ᚖstring(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _Query_me(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -651,6 +828,45 @@ func (ec *executionContext) _Query_beings(ctx context.Context, field graphql.Col
 	return ec.marshalNBeing2ᚕgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeing(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _Query_lotteryWinner(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "Query",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   true,
+		IsResolver: true,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Query_lotteryWinner_args(ctx, rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	fc.Args = args
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Query().LotteryWinner(rctx, args["number"].(int))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(Lucky)
+	fc.Result = res
+	return ec.marshalOLucky2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐLucky(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _Query___type(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -824,6 +1040,38 @@ func (ec *executionContext) _User_luckyNumber(ctx context.Context, field graphql
 	return ec.marshalOInt2ᚖint(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _User_hair(ctx context.Context, field graphql.CollectedField, obj *User) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "User",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   false,
+		IsResolver: false,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Hair, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*Hair)
+	fc.Result = res
+	return ec.marshalOHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐHair(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -1938,6 +2186,22 @@ func (ec *executionContext) _Being(ctx context.Context, sel ast.SelectionSet, ob
 	}
 }
 
+func (ec *executionContext) _Lucky(ctx context.Context, sel ast.SelectionSet, obj Lucky) graphql.Marshaler {
+	switch obj := (obj).(type) {
+	case nil:
+		return graphql.Null
+	case User:
+		return ec._User(ctx, sel, &obj)
+	case *User:
+		if obj == nil {
+			return graphql.Null
+		}
+		return ec._User(ctx, sel, obj)
+	default:
+		panic(fmt.Errorf("unexpected type %T", obj))
+	}
+}
+
 // endregion ************************** interface.gotpl ***************************
 
 // region    **************************** object.gotpl ****************************
@@ -1970,6 +2234,59 @@ func (ec *executionContext) _Animal(ctx context.Context, sel ast.SelectionSet, o
 			}
 		case "owner":
 			out.Values[i] = ec._Animal_owner(ctx, field, obj)
+		case "hair":
+			out.Values[i] = ec._Animal_hair(ctx, field, obj)
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch()
+	if invalids > 0 {
+		return graphql.Null
+	}
+	return out
+}
+
+var beingsHairImplementors = []string{"BeingsHair"}
+
+func (ec *executionContext) _BeingsHair(ctx context.Context, sel ast.SelectionSet, obj *BeingsHair) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, beingsHairImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	var invalids uint32
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("BeingsHair")
+		case "hasHair":
+			out.Values[i] = ec._BeingsHair_hasHair(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalids++
+			}
+		default:
+			panic("unknown field " + strconv.Quote(field.Name))
+		}
+	}
+	out.Dispatch()
+	if invalids > 0 {
+		return graphql.Null
+	}
+	return out
+}
+
+var hairImplementors = []string{"Hair"}
+
+func (ec *executionContext) _Hair(ctx context.Context, sel ast.SelectionSet, obj *Hair) graphql.Marshaler {
+	fields := graphql.CollectFields(ec.OperationContext, sel, hairImplementors)
+
+	out := graphql.NewFieldSet(fields)
+	var invalids uint32
+	for i, field := range fields {
+		switch field.Name {
+		case "__typename":
+			out.Values[i] = graphql.MarshalString("Hair")
+		case "color":
+			out.Values[i] = ec._Hair_color(ctx, field, obj)
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
@@ -2043,6 +2360,17 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
 				}
 				return res
 			})
+		case "lotteryWinner":
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Query_lotteryWinner(ctx, field)
+				return res
+			})
 		case "__type":
 			out.Values[i] = ec._Query___type(ctx, field)
 		case "__schema":
@@ -2058,7 +2386,7 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
 	return out
 }
 
-var userImplementors = []string{"User", "Being"}
+var userImplementors = []string{"User", "Being", "Lucky"}
 
 func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *User) graphql.Marshaler {
 	fields := graphql.CollectFields(ec.OperationContext, sel, userImplementors)
@@ -2081,6 +2409,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
 			}
 		case "luckyNumber":
 			out.Values[i] = ec._User_luckyNumber(ctx, field, obj)
+		case "hair":
+			out.Values[i] = ec._User_hair(ctx, field, obj)
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
@@ -2434,6 +2764,21 @@ func (ec *executionContext) marshalNID2ᚕstringᚄ(ctx context.Context, sel ast
 	return ret
 }
 
+func (ec *executionContext) unmarshalNInt2int(ctx context.Context, v interface{}) (int, error) {
+	res, err := graphql.UnmarshalInt(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNInt2int(ctx context.Context, sel ast.SelectionSet, v int) graphql.Marshaler {
+	res := graphql.MarshalInt(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "must not be null")
+		}
+	}
+	return res
+}
+
 func (ec *executionContext) unmarshalNSpecies2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐSpecies(ctx context.Context, v interface{}) (Species, error) {
 	var res Species
 	err := res.UnmarshalGQL(v)
@@ -2695,6 +3040,13 @@ func (ec *executionContext) marshalOBeing2githubᚗcomᚋKhanᚋgenqlientᚋinte
 	return ec._Being(ctx, sel, v)
 }
 
+func (ec *executionContext) marshalOBeingsHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐBeingsHair(ctx context.Context, sel ast.SelectionSet, v *BeingsHair) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._BeingsHair(ctx, sel, v)
+}
+
 func (ec *executionContext) unmarshalOBoolean2bool(ctx context.Context, v interface{}) (bool, error) {
 	res, err := graphql.UnmarshalBoolean(v)
 	return res, graphql.ErrorOnPath(ctx, err)
@@ -2719,6 +3071,13 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
 	return graphql.MarshalBoolean(*v)
 }
 
+func (ec *executionContext) marshalOHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐHair(ctx context.Context, sel ast.SelectionSet, v *Hair) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._Hair(ctx, sel, v)
+}
+
 func (ec *executionContext) unmarshalOInt2ᚖint(ctx context.Context, v interface{}) (*int, error) {
 	if v == nil {
 		return nil, nil
@@ -2734,6 +3093,13 @@ func (ec *executionContext) marshalOInt2ᚖint(ctx context.Context, sel ast.Sele
 	return graphql.MarshalInt(*v)
 }
 
+func (ec *executionContext) marshalOLucky2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐLucky(ctx context.Context, sel ast.SelectionSet, v Lucky) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return ec._Lucky(ctx, sel, v)
+}
+
 func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) {
 	res, err := graphql.UnmarshalString(v)
 	return res, graphql.ErrorOnPath(ctx, err)
diff --git a/internal/integration/server/gqlgen_models.go b/internal/integration/server/gqlgen_models.go
index a3318fe..a8b225b 100644
--- a/internal/integration/server/gqlgen_models.go
+++ b/internal/integration/server/gqlgen_models.go
@@ -12,22 +12,37 @@ type Being interface {
 	IsBeing()
 }
 
+type Lucky interface {
+	IsLucky()
+}
+
 type Animal struct {
-	ID      string  `json:"id"`
-	Name    string  `json:"name"`
-	Species Species `json:"species"`
-	Owner   Being   `json:"owner"`
+	ID      string      `json:"id"`
+	Name    string      `json:"name"`
+	Species Species     `json:"species"`
+	Owner   Being       `json:"owner"`
+	Hair    *BeingsHair `json:"hair"`
 }
 
 func (Animal) IsBeing() {}
 
+type BeingsHair struct {
+	HasHair bool `json:"hasHair"`
+}
+
+type Hair struct {
+	Color *string `json:"color"`
+}
+
 type User struct {
 	ID          string `json:"id"`
 	Name        string `json:"name"`
 	LuckyNumber *int   `json:"luckyNumber"`
+	Hair        *Hair  `json:"hair"`
 }
 
 func (User) IsBeing() {}
+func (User) IsLucky() {}
 
 type Species string
 
diff --git a/internal/integration/server/server.go b/internal/integration/server/server.go
index 8032326..6cff7cc 100644
--- a/internal/integration/server/server.go
+++ b/internal/integration/server/server.go
@@ -8,16 +8,26 @@ import (
 	"github.com/99designs/gqlgen/graphql/handler/transport"
 )
 
-func intptr(v int) *int { return &v }
+func strptr(v string) *string { return &v }
+func intptr(v int) *int       { return &v }
 
 var users = []*User{
-	{ID: "1", Name: "Yours Truly", LuckyNumber: intptr(17)},
-	{ID: "2", Name: "Raven", LuckyNumber: intptr(-1)},
+	{
+		ID: "1", Name: "Yours Truly", LuckyNumber: intptr(17),
+		Hair: &Hair{Color: strptr("Black")},
+	},
+	{ID: "2", Name: "Raven", LuckyNumber: intptr(-1), Hair: nil},
 }
 
 var animals = []*Animal{
-	{ID: "3", Name: "Fido", Species: SpeciesDog, Owner: userByID("0")},
-	{ID: "4", Name: "Old One", Species: SpeciesCoelacanth, Owner: nil},
+	{
+		ID: "3", Name: "Fido", Species: SpeciesDog, Owner: userByID("1"),
+		Hair: &BeingsHair{HasHair: true},
+	},
+	{
+		ID: "4", Name: "Old One", Species: SpeciesCoelacanth, Owner: nil,
+		Hair: &BeingsHair{HasHair: false},
+	},
 }
 
 func userByID(id string) *User {
@@ -63,6 +73,15 @@ func (r *queryResolver) Beings(ctx context.Context, ids []string) ([]Being, erro
 	return ret, nil
 }
 
+func (r *queryResolver) LotteryWinner(ctx context.Context, number int) (Lucky, error) {
+	for _, user := range users {
+		if user.LuckyNumber != nil && *user.LuckyNumber == number {
+			return user, nil
+		}
+	}
+	return nil, nil
+}
+
 func RunServer() *httptest.Server {
 	gqlgenServer := handler.New(NewExecutableSchema(Config{Resolvers: &resolver{}}))
 	gqlgenServer.AddTransport(transport.POST{})


Test patch:
diff --git a/generate/generate_test.go b/generate/generate_test.go
index a909127..9c21915 100644
--- a/generate/generate_test.go
+++ b/generate/generate_test.go
@@ -52,11 +52,14 @@ func TestGenerate(t *testing.T) {
 				Generated:        goFilename,
 				ExportOperations: queriesFilename,
 				Bindings: map[string]*TypeBinding{
-					"ID":           {Type: "github.com/Khan/genqlient/internal/testutil.ID"},
-					"DateTime":     {Type: "time.Time"},
-					"Junk":         {Type: "interface{}"},
-					"ComplexJunk":  {Type: "[]map[string]*[]*map[string]interface{}"},
-					"Pokemon":      {Type: "github.com/Khan/genqlient/internal/testutil.Pokemon"},
+					"ID":          {Type: "github.com/Khan/genqlient/internal/testutil.ID"},
+					"DateTime":    {Type: "time.Time"},
+					"Junk":        {Type: "interface{}"},
+					"ComplexJunk": {Type: "[]map[string]*[]*map[string]interface{}"},
+					"Pokemon": {
+						Type:              "github.com/Khan/genqlient/internal/testutil.Pokemon",
+						ExpectExactFields: "{ species level }",
+					},
 					"PokemonInput": {Type: "github.com/Khan/genqlient/internal/testutil.Pokemon"},
 				},
 				AllowBrokenFeatures: true,
@@ -117,6 +120,14 @@ func TestGenerate(t *testing.T) {
 	}
 }
 
+// TestGenerate is a snapshot-based test of error text.
+//
+// For each .go or .graphql file in testdata/errors, and corresponding
+// .schema.graphql file, it asserts that the given query returns an error, and
+// that that error's string-text matches the snapshot.  The snapshotting is
+// useful to ensure we don't accidentally make the text less readable, drop the
+// line numbers, etc.  We include both .go and .graphql tests, to make sure the
+// line numbers work in both cases.
 func TestGenerateErrors(t *testing.T) {
 	files, err := ioutil.ReadDir(errorsDir)
 	if err != nil {
@@ -143,6 +154,10 @@ func TestGenerateErrors(t *testing.T) {
 				Bindings: map[string]*TypeBinding{
 					"ValidScalar":   {Type: "string"},
 					"InvalidScalar": {Type: "bogus"},
+					"Pokemon": {
+						Type:              "github.com/Khan/genqlient/internal/testutil.Pokemon",
+						ExpectExactFields: "{ species level }",
+					},
 				},
 				AllowBrokenFeatures: true,
 			})
diff --git a/generate/testdata/errors/BindingWithIncorrectSelection.go b/generate/testdata/errors/BindingWithIncorrectSelection.go
new file mode 100644
index 0000000..9b5ae1a
--- /dev/null
+++ b/generate/testdata/errors/BindingWithIncorrectSelection.go
@@ -0,0 +1,7 @@
+package errors
+
+const _ = `# @genqlient
+query GetPokemonWrongFields {
+  pokemon { species }
+}
+`
diff --git a/generate/testdata/errors/BindingWithIncorrectSelection.graphql b/generate/testdata/errors/BindingWithIncorrectSelection.graphql
new file mode 100644
index 0000000..38eba3d
--- /dev/null
+++ b/generate/testdata/errors/BindingWithIncorrectSelection.graphql
@@ -0,0 +1,3 @@
+query GetPokemonWrongFields {
+  pokemon { species species }
+}
diff --git a/generate/testdata/errors/BindingWithIncorrectSelection.schema.graphql b/generate/testdata/errors/BindingWithIncorrectSelection.schema.graphql
new file mode 100644
index 0000000..ca01037
--- /dev/null
+++ b/generate/testdata/errors/BindingWithIncorrectSelection.schema.graphql
@@ -0,0 +1,8 @@
+type Query {
+  pokemon: Pokemon
+}
+
+type Pokemon {
+  species: String!
+  level: Int!
+}
diff --git a/generate/testdata/errors/ConflictingSelections.go b/generate/testdata/errors/ConflictingSelections.go
new file mode 100644
index 0000000..c8ff6a3
--- /dev/null
+++ b/generate/testdata/errors/ConflictingSelections.go
@@ -0,0 +1,12 @@
+package errors
+
+const _ = `# @genqlient
+query {
+  myField {
+    subField { subSubField1 subSubField2 }
+    ... on OnePossibleConcreteType {
+      subField { subSubField3 subSubField4 }
+    }
+  }
+}
+`
diff --git a/generate/testdata/errors/ConflictingSelections.graphql b/generate/testdata/errors/ConflictingSelections.graphql
new file mode 100644
index 0000000..159624e
--- /dev/null
+++ b/generate/testdata/errors/ConflictingSelections.graphql
@@ -0,0 +1,8 @@
+query {
+  myField {
+    subField { subSubField1 subSubField2 }
+    ... on OnePossibleConcreteType {
+      subField { subSubField3 subSubField4 }
+    }
+  }
+}
diff --git a/generate/testdata/errors/ConflictingSelections.schema.graphql b/generate/testdata/errors/ConflictingSelections.schema.graphql
new file mode 100644
index 0000000..72ed539
--- /dev/null
+++ b/generate/testdata/errors/ConflictingSelections.schema.graphql
@@ -0,0 +1,18 @@
+type Query {
+  myField: InterfaceType
+}
+
+interface InterfaceType {
+  subField: SubFieldType
+}
+
+type OnePossibleConcreteType implements InterfaceType {
+  subField: SubFieldType
+}
+
+type SubFieldType {
+  subSubField1: String!
+  subSubField2: String!
+  subSubField3: String!
+  subSubField4: String!
+}
diff --git a/generate/testdata/queries/ComplexInlineFragments.graphql b/generate/testdata/queries/ComplexInlineFragments.graphql
new file mode 100644
index 0000000..99698f3
--- /dev/null
+++ b/generate/testdata/queries/ComplexInlineFragments.graphql
@@ -0,0 +1,69 @@
+# We test all the spread cases from DESIGN.md, see there for more context on
+# each, as well as various other nonsense.  But for abstract-in-abstract
+# spreads, we can't test cases (4b) and (4c), where I implements J or vice
+# versa, because gqlparser doesn't support interfaces that implement other
+# interfaces yet.
+query ComplexInlineFragments {
+  root {
+    id
+    ... on Topic { schoolGrade }  # (1) object spread in object scope
+    ... on Content { name }       # (3) abstract spread in object scope
+  }
+  randomItem {
+    id
+    ... on Article { text }          # (2) object spread in abstract scope
+    ... on Content { name }          # (4a) abstract spread in abstract scope, I == J
+    ... on HasDuration { duration }  # (4d) abstract spread in abstract scope, neither implements the other
+  }
+  repeatedStuff: randomItem {
+    id
+    id
+    url
+    otherId: id
+    ... on Article {
+      name
+      text
+      otherName: name
+    }
+    ... on Content {
+      id
+      name
+      otherName: name
+    }
+    ... on HasDuration { duration }
+  }
+  conflictingStuff: randomItem {
+    # These two have different types!  Naming gets complicated.  Note GraphQL
+    # says [1] that you can only have such naming conflicts when the fields are
+    # both on object-typed spreads (reasonable, so they can never collide) and
+    # they are of "shapes that can be merged", e.g. both nullable objects,
+    # which seems very strange to me but is the most interesting case for us
+    # anyway (since where we could have trouble is naming the result types).
+    # [1] https://spec.graphql.org/draft/#SameResponseShape()
+    # TODO(benkraft): This actually generates the wrong thing right now (the
+    # two thumbnail types get the same name, and one clobbers the other).  Fix
+    # in a follow-up commit.
+    ... on Article { thumbnail { id thumbnailUrl } }
+    ... on Video { thumbnail { id timestampSec } }
+  }
+  nestedStuff: randomItem {
+    ... on Topic {
+      children {
+        id
+        ... on Article {
+          text
+          parent {
+            ... on Content {
+              name
+              parent {
+                ... on Topic {
+                  children { id name }
+                }
+              }
+            }
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/generate/testdata/queries/Pokemon.graphql b/generate/testdata/queries/Pokemon.graphql
index 5b40414..f41db71 100644
--- a/generate/testdata/queries/Pokemon.graphql
+++ b/generate/testdata/queries/Pokemon.graphql
@@ -8,7 +8,8 @@ query GetPokemonSiblings($input: PokemonInput!) {
     roles
     name
     # this is mapped globally to internal/testutil.Pokemon:
-    pokemon { species level }
+    # note field ordering matters, but whitespace shouldn't.
+    pokemon {    species level }
     # this overrides said mapping:
     # @genqlient(bind: "-")
     genqlientPokemon: pokemon { species level }
diff --git a/generate/testdata/queries/SimpleInlineFragment.graphql b/generate/testdata/queries/SimpleInlineFragment.graphql
new file mode 100644
index 0000000..f2b75f6
--- /dev/null
+++ b/generate/testdata/queries/SimpleInlineFragment.graphql
@@ -0,0 +1,8 @@
+query SimpleInlineFragment {
+  randomItem {
+    id
+    name
+    ... on Article { text }
+    ... on Video { duration }
+  }
+}
diff --git a/generate/testdata/queries/schema.graphql b/generate/testdata/queries/schema.graphql
index feaf9b2..d3c940c 100644
--- a/generate/testdata/queries/schema.graphql
+++ b/generate/testdata/queries/schema.graphql
@@ -65,6 +65,13 @@ type User {
   emailsWithNullsOrNull: [String]
   authMethods: [AuthMethod!]!
   pokemon: [Pokemon!]
+  greeting: Clip
+}
+
+"""An audio clip, such as of a user saying hello."""
+type Clip implements HasDuration {
+  id: ID!
+  duration: Int!
 }
 
 """Content is implemented by various types like Article, Video, and Topic."""
@@ -73,6 +80,12 @@ interface Content {
   id: ID!
   name: String!
   parent: Topic
+  url: String!
+}
+
+"""An object with a duration, like a video."""
+interface HasDuration {
+  duration: Int!
 }
 
 """LeafContent represents content items that can't have child-nodes."""
@@ -83,15 +96,29 @@ type Article implements Content {
   id: ID!
   name: String!
   parent: Topic!
+  url: String!
   text: String!
+  thumbnail: StuffThumbnail
 }
 
-type Video implements Content {
+type StuffThumbnail {  # for articles, but let's give the name-generator a hard time.
+  id: ID!
+  thumbnailUrl: String!
+}
+
+type Video implements Content & HasDuration {
   """ID is documented in the Content interface."""
   id: ID!
   name: String!
   parent: Topic!
+  url: String!
   duration: Int!
+  thumbnail: Thumbnail
+}
+
+type Thumbnail {  # for videos, but let's give the name-generator a hard time.
+  id: ID!
+  timestampSec: Int!
 }
 
 type Topic implements Content {
@@ -99,7 +126,9 @@ type Topic implements Content {
   id: ID!
   name: String!
   parent: Topic
+  url: String!
   children: [Content!]!
+  schoolGrade: String
 }
 
 """Query's description is probably ignored by almost all callers."""
diff --git a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go
new file mode 100644
index 0000000..d667050
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go
@@ -0,0 +1,963 @@
+package test
+
+// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/Khan/genqlient/graphql"
+	"github.com/Khan/genqlient/internal/testutil"
+)
+
+// ComplexInlineFragmentsConflictingStuffArticle includes the requested fields of the GraphQL type Article.
+type ComplexInlineFragmentsConflictingStuffArticle struct {
+	Typename  string                                          `json:"__typename"`
+	Thumbnail ComplexInlineFragmentsConflictingStuffThumbnail `json:"thumbnail"`
+}
+
+// ComplexInlineFragmentsConflictingStuffContent includes the requested fields of the GraphQL interface Content.
+//
+// ComplexInlineFragmentsConflictingStuffContent is implemented by the following types:
+// ComplexInlineFragmentsConflictingStuffArticle
+// ComplexInlineFragmentsConflictingStuffVideo
+// ComplexInlineFragmentsConflictingStuffTopic
+//
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type ComplexInlineFragmentsConflictingStuffContent interface {
+	implementsGraphQLInterfaceComplexInlineFragmentsConflictingStuffContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+}
+
+func (v *ComplexInlineFragmentsConflictingStuffArticle) implementsGraphQLInterfaceComplexInlineFragmentsConflictingStuffContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsConflictingStuffContent.
+func (v *ComplexInlineFragmentsConflictingStuffArticle) GetTypename() string { return v.Typename }
+
+func (v *ComplexInlineFragmentsConflictingStuffVideo) implementsGraphQLInterfaceComplexInlineFragmentsConflictingStuffContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsConflictingStuffContent.
+func (v *ComplexInlineFragmentsConflictingStuffVideo) GetTypename() string { return v.Typename }
+
+func (v *ComplexInlineFragmentsConflictingStuffTopic) implementsGraphQLInterfaceComplexInlineFragmentsConflictingStuffContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsConflictingStuffContent.
+func (v *ComplexInlineFragmentsConflictingStuffTopic) GetTypename() string { return v.Typename }
+
+func __unmarshalComplexInlineFragmentsConflictingStuffContent(v *ComplexInlineFragmentsConflictingStuffContent, m json.RawMessage) error {
+	if string(m) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(m, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "Article":
+		*v = new(ComplexInlineFragmentsConflictingStuffArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(ComplexInlineFragmentsConflictingStuffVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(ComplexInlineFragmentsConflictingStuffTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for ComplexInlineFragmentsConflictingStuffContent: "%v"`, tn.TypeName)
+	}
+}
+
+// ComplexInlineFragmentsConflictingStuffThumbnail includes the requested fields of the GraphQL type Thumbnail.
+type ComplexInlineFragmentsConflictingStuffThumbnail struct {
+	Id           testutil.ID `json:"id"`
+	TimestampSec int         `json:"timestampSec"`
+}
+
+// ComplexInlineFragmentsConflictingStuffTopic includes the requested fields of the GraphQL type Topic.
+type ComplexInlineFragmentsConflictingStuffTopic struct {
+	Typename string `json:"__typename"`
+}
+
+// ComplexInlineFragmentsConflictingStuffVideo includes the requested fields of the GraphQL type Video.
+type ComplexInlineFragmentsConflictingStuffVideo struct {
+	Typename  string                                          `json:"__typename"`
+	Thumbnail ComplexInlineFragmentsConflictingStuffThumbnail `json:"thumbnail"`
+}
+
+// ComplexInlineFragmentsNestedStuffArticle includes the requested fields of the GraphQL type Article.
+type ComplexInlineFragmentsNestedStuffArticle struct {
+	Typename string `json:"__typename"`
+}
+
+// ComplexInlineFragmentsNestedStuffChildrenArticle includes the requested fields of the GraphQL type Article.
+type ComplexInlineFragmentsNestedStuffChildrenArticle struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id     testutil.ID                                          `json:"id"`
+	Text   string                                               `json:"text"`
+	Parent ComplexInlineFragmentsNestedStuffChildrenParentTopic `json:"parent"`
+}
+
+// ComplexInlineFragmentsNestedStuffChildrenContent includes the requested fields of the GraphQL interface Content.
+//
+// ComplexInlineFragmentsNestedStuffChildrenContent is implemented by the following types:
+// ComplexInlineFragmentsNestedStuffChildrenArticle
+// ComplexInlineFragmentsNestedStuffChildrenVideo
+// ComplexInlineFragmentsNestedStuffChildrenTopic
+//
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type ComplexInlineFragmentsNestedStuffChildrenContent interface {
+	implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffChildrenContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+	// GetId returns the interface-field "id" from its implementation.
+	// The GraphQL interface field's documentation follows.
+	//
+	// ID is the identifier of the content.
+	GetId() testutil.ID
+}
+
+func (v *ComplexInlineFragmentsNestedStuffChildrenArticle) implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenArticle) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenArticle) GetId() testutil.ID { return v.Id }
+
+func (v *ComplexInlineFragmentsNestedStuffChildrenVideo) implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenVideo) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenVideo) GetId() testutil.ID { return v.Id }
+
+func (v *ComplexInlineFragmentsNestedStuffChildrenTopic) implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenTopic) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenTopic) GetId() testutil.ID { return v.Id }
+
+func __unmarshalComplexInlineFragmentsNestedStuffChildrenContent(v *ComplexInlineFragmentsNestedStuffChildrenContent, m json.RawMessage) error {
+	if string(m) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(m, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "Article":
+		*v = new(ComplexInlineFragmentsNestedStuffChildrenArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(ComplexInlineFragmentsNestedStuffChildrenVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(ComplexInlineFragmentsNestedStuffChildrenTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for ComplexInlineFragmentsNestedStuffChildrenContent: "%v"`, tn.TypeName)
+	}
+}
+
+// ComplexInlineFragmentsNestedStuffChildrenParentTopic includes the requested fields of the GraphQL type Topic.
+type ComplexInlineFragmentsNestedStuffChildrenParentTopic struct {
+	Name   string                                                          `json:"name"`
+	Parent ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopic `json:"parent"`
+}
+
+// ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopic includes the requested fields of the GraphQL type Topic.
+type ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopic struct {
+	Children []ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent `json:"-"`
+}
+
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopic) UnmarshalJSON(b []byte) error {
+
+	type ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicWrapper ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopic
+
+	var firstPass struct {
+		*ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicWrapper
+		Children []json.RawMessage `json:"children"`
+	}
+	firstPass.ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicWrapper = (*ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicWrapper)(v)
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.Children
+		raw := firstPass.Children
+		*target = make(
+			[]ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent,
+			len(raw))
+		for i, raw := range raw {
+			target := &(*target)[i]
+			err = __unmarshalComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent(
+				target, raw)
+			if err != nil {
+				return fmt.Errorf(
+					"Unable to unmarshal ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopic.Children: %w", err)
+			}
+		}
+	}
+	return nil
+}
+
+// ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenArticle includes the requested fields of the GraphQL type Article.
+type ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenArticle struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent includes the requested fields of the GraphQL interface Content.
+//
+// ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent is implemented by the following types:
+// ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenArticle
+// ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenVideo
+// ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenTopic
+//
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent interface {
+	implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+	// GetId returns the interface-field "id" from its implementation.
+	// The GraphQL interface field's documentation follows.
+	//
+	// ID is the identifier of the content.
+	GetId() testutil.ID
+	// GetName returns the interface-field "name" from its implementation.
+	GetName() string
+}
+
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenArticle) implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenArticle) GetTypename() string {
+	return v.Typename
+}
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenArticle) GetId() testutil.ID {
+	return v.Id
+}
+
+// GetName is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenArticle) GetName() string {
+	return v.Name
+}
+
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenVideo) implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenVideo) GetTypename() string {
+	return v.Typename
+}
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenVideo) GetId() testutil.ID {
+	return v.Id
+}
+
+// GetName is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenVideo) GetName() string {
+	return v.Name
+}
+
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenTopic) implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenTopic) GetTypename() string {
+	return v.Typename
+}
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenTopic) GetId() testutil.ID {
+	return v.Id
+}
+
+// GetName is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent.
+func (v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenTopic) GetName() string {
+	return v.Name
+}
+
+func __unmarshalComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent(v *ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent, m json.RawMessage) error {
+	if string(m) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(m, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "Article":
+		*v = new(ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenContent: "%v"`, tn.TypeName)
+	}
+}
+
+// ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenTopic includes the requested fields of the GraphQL type Topic.
+type ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenTopic struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenVideo includes the requested fields of the GraphQL type Video.
+type ComplexInlineFragmentsNestedStuffChildrenParentTopicParentTopicChildrenVideo struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// ComplexInlineFragmentsNestedStuffChildrenTopic includes the requested fields of the GraphQL type Topic.
+type ComplexInlineFragmentsNestedStuffChildrenTopic struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id testutil.ID `json:"id"`
+}
+
+// ComplexInlineFragmentsNestedStuffChildrenVideo includes the requested fields of the GraphQL type Video.
+type ComplexInlineFragmentsNestedStuffChildrenVideo struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id testutil.ID `json:"id"`
+}
+
+// ComplexInlineFragmentsNestedStuffContent includes the requested fields of the GraphQL interface Content.
+//
+// ComplexInlineFragmentsNestedStuffContent is implemented by the following types:
+// ComplexInlineFragmentsNestedStuffArticle
+// ComplexInlineFragmentsNestedStuffVideo
+// ComplexInlineFragmentsNestedStuffTopic
+//
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type ComplexInlineFragmentsNestedStuffContent interface {
+	implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+}
+
+func (v *ComplexInlineFragmentsNestedStuffArticle) implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffContent.
+func (v *ComplexInlineFragmentsNestedStuffArticle) GetTypename() string { return v.Typename }
+
+func (v *ComplexInlineFragmentsNestedStuffVideo) implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffContent.
+func (v *ComplexInlineFragmentsNestedStuffVideo) GetTypename() string { return v.Typename }
+
+func (v *ComplexInlineFragmentsNestedStuffTopic) implementsGraphQLInterfaceComplexInlineFragmentsNestedStuffContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffContent.
+func (v *ComplexInlineFragmentsNestedStuffTopic) GetTypename() string { return v.Typename }
+
+func __unmarshalComplexInlineFragmentsNestedStuffContent(v *ComplexInlineFragmentsNestedStuffContent, m json.RawMessage) error {
+	if string(m) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(m, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "Article":
+		*v = new(ComplexInlineFragmentsNestedStuffArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(ComplexInlineFragmentsNestedStuffVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(ComplexInlineFragmentsNestedStuffTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for ComplexInlineFragmentsNestedStuffContent: "%v"`, tn.TypeName)
+	}
+}
+
+// ComplexInlineFragmentsNestedStuffTopic includes the requested fields of the GraphQL type Topic.
+type ComplexInlineFragmentsNestedStuffTopic struct {
+	Typename string                                             `json:"__typename"`
+	Children []ComplexInlineFragmentsNestedStuffChildrenContent `json:"-"`
+}
+
+func (v *ComplexInlineFragmentsNestedStuffTopic) UnmarshalJSON(b []byte) error {
+
+	type ComplexInlineFragmentsNestedStuffTopicWrapper ComplexInlineFragmentsNestedStuffTopic
+
+	var firstPass struct {
+		*ComplexInlineFragmentsNestedStuffTopicWrapper
+		Children []json.RawMessage `json:"children"`
+	}
+	firstPass.ComplexInlineFragmentsNestedStuffTopicWrapper = (*ComplexInlineFragmentsNestedStuffTopicWrapper)(v)
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.Children
+		raw := firstPass.Children
+		*target = make(
+			[]ComplexInlineFragmentsNestedStuffChildrenContent,
+			len(raw))
+		for i, raw := range raw {
+			target := &(*target)[i]
+			err = __unmarshalComplexInlineFragmentsNestedStuffChildrenContent(
+				target, raw)
+			if err != nil {
+				return fmt.Errorf(
+					"Unable to unmarshal ComplexInlineFragmentsNestedStuffTopic.Children: %w", err)
+			}
+		}
+	}
+	return nil
+}
+
+// ComplexInlineFragmentsNestedStuffVideo includes the requested fields of the GraphQL type Video.
+type ComplexInlineFragmentsNestedStuffVideo struct {
+	Typename string `json:"__typename"`
+}
+
+// ComplexInlineFragmentsRandomItemArticle includes the requested fields of the GraphQL type Article.
+type ComplexInlineFragmentsRandomItemArticle struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Text string      `json:"text"`
+	Name string      `json:"name"`
+}
+
+// ComplexInlineFragmentsRandomItemContent includes the requested fields of the GraphQL interface Content.
+//
+// ComplexInlineFragmentsRandomItemContent is implemented by the following types:
+// ComplexInlineFragmentsRandomItemArticle
+// ComplexInlineFragmentsRandomItemVideo
+// ComplexInlineFragmentsRandomItemTopic
+//
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type ComplexInlineFragmentsRandomItemContent interface {
+	implementsGraphQLInterfaceComplexInlineFragmentsRandomItemContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+	// GetId returns the interface-field "id" from its implementation.
+	// The GraphQL interface field's documentation follows.
+	//
+	// ID is the identifier of the content.
+	GetId() testutil.ID
+	// GetName returns the interface-field "name" from its implementation.
+	GetName() string
+}
+
+func (v *ComplexInlineFragmentsRandomItemArticle) implementsGraphQLInterfaceComplexInlineFragmentsRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsRandomItemContent.
+func (v *ComplexInlineFragmentsRandomItemArticle) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsRandomItemContent.
+func (v *ComplexInlineFragmentsRandomItemArticle) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface ComplexInlineFragmentsRandomItemContent.
+func (v *ComplexInlineFragmentsRandomItemArticle) GetName() string { return v.Name }
+
+func (v *ComplexInlineFragmentsRandomItemVideo) implementsGraphQLInterfaceComplexInlineFragmentsRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsRandomItemContent.
+func (v *ComplexInlineFragmentsRandomItemVideo) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsRandomItemContent.
+func (v *ComplexInlineFragmentsRandomItemVideo) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface ComplexInlineFragmentsRandomItemContent.
+func (v *ComplexInlineFragmentsRandomItemVideo) GetName() string { return v.Name }
+
+func (v *ComplexInlineFragmentsRandomItemTopic) implementsGraphQLInterfaceComplexInlineFragmentsRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsRandomItemContent.
+func (v *ComplexInlineFragmentsRandomItemTopic) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsRandomItemContent.
+func (v *ComplexInlineFragmentsRandomItemTopic) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface ComplexInlineFragmentsRandomItemContent.
+func (v *ComplexInlineFragmentsRandomItemTopic) GetName() string { return v.Name }
+
+func __unmarshalComplexInlineFragmentsRandomItemContent(v *ComplexInlineFragmentsRandomItemContent, m json.RawMessage) error {
+	if string(m) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(m, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "Article":
+		*v = new(ComplexInlineFragmentsRandomItemArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(ComplexInlineFragmentsRandomItemVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(ComplexInlineFragmentsRandomItemTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for ComplexInlineFragmentsRandomItemContent: "%v"`, tn.TypeName)
+	}
+}
+
+// ComplexInlineFragmentsRandomItemTopic includes the requested fields of the GraphQL type Topic.
+type ComplexInlineFragmentsRandomItemTopic struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// ComplexInlineFragmentsRandomItemVideo includes the requested fields of the GraphQL type Video.
+type ComplexInlineFragmentsRandomItemVideo struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id       testutil.ID `json:"id"`
+	Name     string      `json:"name"`
+	Duration int         `json:"duration"`
+}
+
+// ComplexInlineFragmentsRepeatedStuffArticle includes the requested fields of the GraphQL type Article.
+type ComplexInlineFragmentsRepeatedStuffArticle struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id  testutil.ID `json:"id"`
+	Url string      `json:"url"`
+	// ID is the identifier of the content.
+	OtherId   testutil.ID `json:"otherId"`
+	Name      string      `json:"name"`
+	Text      string      `json:"text"`
+	OtherName string      `json:"otherName"`
+}
+
+// ComplexInlineFragmentsRepeatedStuffContent includes the requested fields of the GraphQL interface Content.
+//
+// ComplexInlineFragmentsRepeatedStuffContent is implemented by the following types:
+// ComplexInlineFragmentsRepeatedStuffArticle
+// ComplexInlineFragmentsRepeatedStuffVideo
+// ComplexInlineFragmentsRepeatedStuffTopic
+//
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type ComplexInlineFragmentsRepeatedStuffContent interface {
+	implementsGraphQLInterfaceComplexInlineFragmentsRepeatedStuffContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+	// GetId returns the interface-field "id" from its implementation.
+	// The GraphQL interface field's documentation follows.
+	//
+	// ID is the identifier of the content.
+	GetId() testutil.ID
+	// GetUrl returns the interface-field "url" from its implementation.
+	GetUrl() string
+	// GetOtherId returns the interface-field "id" from its implementation.
+	// The GraphQL interface field's documentation follows.
+	//
+	// ID is the identifier of the content.
+	GetOtherId() testutil.ID
+	// GetName returns the interface-field "name" from its implementation.
+	GetName() string
+	// GetOtherName returns the interface-field "name" from its implementation.
+	GetOtherName() string
+}
+
+func (v *ComplexInlineFragmentsRepeatedStuffArticle) implementsGraphQLInterfaceComplexInlineFragmentsRepeatedStuffContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffArticle) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffArticle) GetId() testutil.ID { return v.Id }
+
+// GetUrl is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffArticle) GetUrl() string { return v.Url }
+
+// GetOtherId is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffArticle) GetOtherId() testutil.ID { return v.OtherId }
+
+// GetName is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffArticle) GetName() string { return v.Name }
+
+// GetOtherName is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffArticle) GetOtherName() string { return v.OtherName }
+
+func (v *ComplexInlineFragmentsRepeatedStuffVideo) implementsGraphQLInterfaceComplexInlineFragmentsRepeatedStuffContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffVideo) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffVideo) GetId() testutil.ID { return v.Id }
+
+// GetUrl is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffVideo) GetUrl() string { return v.Url }
+
+// GetOtherId is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffVideo) GetOtherId() testutil.ID { return v.OtherId }
+
+// GetName is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffVideo) GetName() string { return v.Name }
+
+// GetOtherName is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffVideo) GetOtherName() string { return v.OtherName }
+
+func (v *ComplexInlineFragmentsRepeatedStuffTopic) implementsGraphQLInterfaceComplexInlineFragmentsRepeatedStuffContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffTopic) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffTopic) GetId() testutil.ID { return v.Id }
+
+// GetUrl is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffTopic) GetUrl() string { return v.Url }
+
+// GetOtherId is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffTopic) GetOtherId() testutil.ID { return v.OtherId }
+
+// GetName is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffTopic) GetName() string { return v.Name }
+
+// GetOtherName is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
+func (v *ComplexInlineFragmentsRepeatedStuffTopic) GetOtherName() string { return v.OtherName }
+
+func __unmarshalComplexInlineFragmentsRepeatedStuffContent(v *ComplexInlineFragmentsRepeatedStuffContent, m json.RawMessage) error {
+	if string(m) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(m, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "Article":
+		*v = new(ComplexInlineFragmentsRepeatedStuffArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(ComplexInlineFragmentsRepeatedStuffVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(ComplexInlineFragmentsRepeatedStuffTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for ComplexInlineFragmentsRepeatedStuffContent: "%v"`, tn.TypeName)
+	}
+}
+
+// ComplexInlineFragmentsRepeatedStuffTopic includes the requested fields of the GraphQL type Topic.
+type ComplexInlineFragmentsRepeatedStuffTopic struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id  testutil.ID `json:"id"`
+	Url string      `json:"url"`
+	// ID is the identifier of the content.
+	OtherId   testutil.ID `json:"otherId"`
+	Name      string      `json:"name"`
+	OtherName string      `json:"otherName"`
+}
+
+// ComplexInlineFragmentsRepeatedStuffVideo includes the requested fields of the GraphQL type Video.
+type ComplexInlineFragmentsRepeatedStuffVideo struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id  testutil.ID `json:"id"`
+	Url string      `json:"url"`
+	// ID is the identifier of the content.
+	OtherId   testutil.ID `json:"otherId"`
+	Name      string      `json:"name"`
+	OtherName string      `json:"otherName"`
+	Duration  int         `json:"duration"`
+}
+
+// ComplexInlineFragmentsResponse is returned by ComplexInlineFragments on success.
+type ComplexInlineFragmentsResponse struct {
+	Root             ComplexInlineFragmentsRootTopic               `json:"root"`
+	RandomItem       ComplexInlineFragmentsRandomItemContent       `json:"-"`
+	RepeatedStuff    ComplexInlineFragmentsRepeatedStuffContent    `json:"-"`
+	ConflictingStuff ComplexInlineFragmentsConflictingStuffContent `json:"-"`
+	NestedStuff      ComplexInlineFragmentsNestedStuffContent      `json:"-"`
+}
+
+func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
+
+	type ComplexInlineFragmentsResponseWrapper ComplexInlineFragmentsResponse
+
+	var firstPass struct {
+		*ComplexInlineFragmentsResponseWrapper
+		RandomItem       json.RawMessage `json:"randomItem"`
+		RepeatedStuff    json.RawMessage `json:"repeatedStuff"`
+		ConflictingStuff json.RawMessage `json:"conflictingStuff"`
+		NestedStuff      json.RawMessage `json:"nestedStuff"`
+	}
+	firstPass.ComplexInlineFragmentsResponseWrapper = (*ComplexInlineFragmentsResponseWrapper)(v)
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.RandomItem
+		raw := firstPass.RandomItem
+		err = __unmarshalComplexInlineFragmentsRandomItemContent(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal ComplexInlineFragmentsResponse.RandomItem: %w", err)
+		}
+	}
+	{
+		target := &v.RepeatedStuff
+		raw := firstPass.RepeatedStuff
+		err = __unmarshalComplexInlineFragmentsRepeatedStuffContent(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal ComplexInlineFragmentsResponse.RepeatedStuff: %w", err)
+		}
+	}
+	{
+		target := &v.ConflictingStuff
+		raw := firstPass.ConflictingStuff
+		err = __unmarshalComplexInlineFragmentsConflictingStuffContent(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal ComplexInlineFragmentsResponse.ConflictingStuff: %w", err)
+		}
+	}
+	{
+		target := &v.NestedStuff
+		raw := firstPass.NestedStuff
+		err = __unmarshalComplexInlineFragmentsNestedStuffContent(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal ComplexInlineFragmentsResponse.NestedStuff: %w", err)
+		}
+	}
+	return nil
+}
+
+// ComplexInlineFragmentsRootTopic includes the requested fields of the GraphQL type Topic.
+type ComplexInlineFragmentsRootTopic struct {
+	// ID is documented in the Content interface.
+	Id          testutil.ID `json:"id"`
+	SchoolGrade string      `json:"schoolGrade"`
+	Name        string      `json:"name"`
+}
+
+// We test all the spread cases from DESIGN.md, see there for more context on
+// each, as well as various other nonsense.  But for abstract-in-abstract
+// spreads, we can't test cases (4b) and (4c), where I implements J or vice
+// versa, because gqlparser doesn't support interfaces that implement other
+// interfaces yet.
+func ComplexInlineFragments(
+	client graphql.Client,
+) (*ComplexInlineFragmentsResponse, error) {
+	var retval ComplexInlineFragmentsResponse
+	err := client.MakeRequest(
+		nil,
+		"ComplexInlineFragments",
+		`
+query ComplexInlineFragments {
+	root {
+		id
+		... on Topic {
+			schoolGrade
+		}
+		... on Content {
+			name
+		}
+	}
+	randomItem {
+		__typename
+		id
+		... on Article {
+			text
+		}
+		... on Content {
+			name
+		}
+		... on HasDuration {
+			duration
+		}
+	}
+	repeatedStuff: randomItem {
+		__typename
+		id
+		id
+		url
+		otherId: id
+		... on Article {
+			name
+			text
+			otherName: name
+		}
+		... on Content {
+			id
+			name
+			otherName: name
+		}
+		... on HasDuration {
+			duration
+		}
+	}
+	conflictingStuff: randomItem {
+		__typename
+		... on Article {
+			thumbnail {
+				id
+				thumbnailUrl
+			}
+		}
+		... on Video {
+			thumbnail {
+				id
+				timestampSec
+			}
+		}
+	}
+	nestedStuff: randomItem {
+		__typename
+		... on Topic {
+			children {
+				__typename
+				id
+				... on Article {
+					text
+					parent {
+						... on Content {
+							name
+							parent {
+								... on Topic {
+									children {
+										__typename
+										id
+										name
+									}
+								}
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+}
+`,
+		&retval,
+		nil,
+	)
+	return &retval, err
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.json b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.json
new file mode 100644
index 0000000..357c889
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "ComplexInlineFragments",
+      "query": "\nquery ComplexInlineFragments {\n\troot {\n\t\tid\n\t\t... on Topic {\n\t\t\tschoolGrade\n\t\t}\n\t\t... on Content {\n\t\t\tname\n\t\t}\n\t}\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\t... on Article {\n\t\t\ttext\n\t\t}\n\t\t... on Content {\n\t\t\tname\n\t\t}\n\t\t... on HasDuration {\n\t\t\tduration\n\t\t}\n\t}\n\trepeatedStuff: randomItem {\n\t\t__typename\n\t\tid\n\t\tid\n\t\turl\n\t\totherId: id\n\t\t... on Article {\n\t\t\tname\n\t\t\ttext\n\t\t\totherName: name\n\t\t}\n\t\t... on Content {\n\t\t\tid\n\t\t\tname\n\t\t\totherName: name\n\t\t}\n\t\t... on HasDuration {\n\t\t\tduration\n\t\t}\n\t}\n\tconflictingStuff: randomItem {\n\t\t__typename\n\t\t... on Article {\n\t\t\tthumbnail {\n\t\t\t\tid\n\t\t\t\tthumbnailUrl\n\t\t\t}\n\t\t}\n\t\t... on Video {\n\t\t\tthumbnail {\n\t\t\t\tid\n\t\t\t\ttimestampSec\n\t\t\t}\n\t\t}\n\t}\n\tnestedStuff: randomItem {\n\t\t__typename\n\t\t... on Topic {\n\t\t\tchildren {\n\t\t\t\t__typename\n\t\t\t\tid\n\t\t\t\t... on Article {\n\t\t\t\t\ttext\n\t\t\t\t\tparent {\n\t\t\t\t\t\t... on Content {\n\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\tparent {\n\t\t\t\t\t\t\t\t... on Topic {\n\t\t\t\t\t\t\t\t\tchildren {\n\t\t\t\t\t\t\t\t\t\t__typename\n\t\t\t\t\t\t\t\t\t\tid\n\t\t\t\t\t\t\t\t\t\tname\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n",
+      "sourceLocation": "testdata/queries/ComplexInlineFragments.graphql"
+    }
+  ]
+}
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go
index df0cea5..c5619bd 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go
@@ -50,7 +50,8 @@ func (v *InterfaceListFieldRootTopic) UnmarshalJSON(b []byte) error {
 			err = __unmarshalInterfaceListFieldRootTopicChildrenContent(
 				target, raw)
 			if err != nil {
-				return err
+				return fmt.Errorf(
+					"Unable to unmarshal InterfaceListFieldRootTopic.Children: %w", err)
 			}
 		}
 	}
@@ -147,6 +148,9 @@ func __unmarshalInterfaceListFieldRootTopicChildrenContent(v *InterfaceListField
 	case "Topic":
 		*v = new(InterfaceListFieldRootTopicChildrenTopic)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for InterfaceListFieldRootTopicChildrenContent: "%v"`, tn.TypeName)
@@ -203,7 +207,8 @@ func (v *InterfaceListFieldWithPointerTopic) UnmarshalJSON(b []byte) error {
 			err = __unmarshalInterfaceListFieldWithPointerTopicChildrenContent(
 				target, raw)
 			if err != nil {
-				return err
+				return fmt.Errorf(
+					"Unable to unmarshal InterfaceListFieldWithPointerTopic.Children: %w", err)
 			}
 		}
 	}
@@ -300,6 +305,9 @@ func __unmarshalInterfaceListFieldWithPointerTopicChildrenContent(v *InterfaceLi
 	case "Topic":
 		*v = new(InterfaceListFieldWithPointerTopicChildrenTopic)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for InterfaceListFieldWithPointerTopicChildrenContent: "%v"`, tn.TypeName)
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go
index f91a299..ffcc237 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go
@@ -110,6 +110,9 @@ func __unmarshalInterfaceListOfListOfListsFieldListOfListsOfListsOfContent(v *In
 	case "Topic":
 		*v = new(InterfaceListOfListOfListsFieldListOfListsOfListsOfContentTopic)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for InterfaceListOfListOfListsFieldListOfListsOfListsOfContent: "%v"`, tn.TypeName)
@@ -183,7 +186,8 @@ func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error
 					err = __unmarshalInterfaceListOfListOfListsFieldListOfListsOfListsOfContent(
 						target, raw)
 					if err != nil {
-						return err
+						return fmt.Errorf(
+							"Unable to unmarshal InterfaceListOfListOfListsFieldResponse.ListOfListsOfListsOfContent: %w", err)
 					}
 				}
 			}
@@ -211,7 +215,8 @@ func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error
 					err = __unmarshalInterfaceListOfListOfListsFieldWithPointerContent(
 						*target, raw)
 					if err != nil {
-						return err
+						return fmt.Errorf(
+							"Unable to unmarshal InterfaceListOfListOfListsFieldResponse.WithPointer: %w", err)
 					}
 				}
 			}
@@ -310,6 +315,9 @@ func __unmarshalInterfaceListOfListOfListsFieldWithPointerContent(v *InterfaceLi
 	case "Topic":
 		*v = new(InterfaceListOfListOfListsFieldWithPointerTopic)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for InterfaceListOfListOfListsFieldWithPointerContent: "%v"`, tn.TypeName)
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go
index 336bf1d..1cb1d33 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go
@@ -48,7 +48,8 @@ func (v *InterfaceNestingRootTopic) UnmarshalJSON(b []byte) error {
 			err = __unmarshalInterfaceNestingRootTopicChildrenContent(
 				target, raw)
 			if err != nil {
-				return err
+				return fmt.Errorf(
+					"Unable to unmarshal InterfaceNestingRootTopic.Children: %w", err)
 			}
 		}
 	}
@@ -151,6 +152,9 @@ func __unmarshalInterfaceNestingRootTopicChildrenContent(v *InterfaceNestingRoot
 	case "Topic":
 		*v = new(InterfaceNestingRootTopicChildrenTopic)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for InterfaceNestingRootTopicChildrenContent: "%v"`, tn.TypeName)
@@ -190,7 +194,8 @@ func (v *InterfaceNestingRootTopicChildrenParentTopic) UnmarshalJSON(b []byte) e
 			err = __unmarshalInterfaceNestingRootTopicChildrenParentTopicChildrenContent(
 				target, raw)
 			if err != nil {
-				return err
+				return fmt.Errorf(
+					"Unable to unmarshal InterfaceNestingRootTopicChildrenParentTopic.Children: %w", err)
 			}
 		}
 	}
@@ -283,6 +288,9 @@ func __unmarshalInterfaceNestingRootTopicChildrenParentTopicChildrenContent(v *I
 	case "Topic":
 		*v = new(InterfaceNestingRootTopicChildrenParentTopicChildrenTopic)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for InterfaceNestingRootTopicChildrenParentTopicChildrenContent: "%v"`, tn.TypeName)
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go
index a581c53..10b0aa1 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go
@@ -100,6 +100,9 @@ func __unmarshalInterfaceNoFragmentsQueryRandomItemContent(v *InterfaceNoFragmen
 	case "Topic":
 		*v = new(InterfaceNoFragmentsQueryRandomItemTopic)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for InterfaceNoFragmentsQueryRandomItemContent: "%v"`, tn.TypeName)
@@ -218,6 +221,9 @@ func __unmarshalInterfaceNoFragmentsQueryRandomItemWithTypeNameContent(v *Interf
 	case "Topic":
 		*v = new(InterfaceNoFragmentsQueryRandomItemWithTypeNameTopic)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for InterfaceNoFragmentsQueryRandomItemWithTypeNameContent: "%v"`, tn.TypeName)
@@ -271,7 +277,8 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 		err = __unmarshalInterfaceNoFragmentsQueryRandomItemContent(
 			target, raw)
 		if err != nil {
-			return err
+			return fmt.Errorf(
+				"Unable to unmarshal InterfaceNoFragmentsQueryResponse.RandomItem: %w", err)
 		}
 	}
 	{
@@ -280,7 +287,8 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 		err = __unmarshalInterfaceNoFragmentsQueryRandomItemWithTypeNameContent(
 			target, raw)
 		if err != nil {
-			return err
+			return fmt.Errorf(
+				"Unable to unmarshal InterfaceNoFragmentsQueryResponse.RandomItemWithTypeName: %w", err)
 		}
 	}
 	{
@@ -290,7 +298,8 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 		err = __unmarshalInterfaceNoFragmentsQueryWithPointerContent(
 			*target, raw)
 		if err != nil {
-			return err
+			return fmt.Errorf(
+				"Unable to unmarshal InterfaceNoFragmentsQueryResponse.WithPointer: %w", err)
 		}
 	}
 	return nil
@@ -393,6 +402,9 @@ func __unmarshalInterfaceNoFragmentsQueryWithPointerContent(v *InterfaceNoFragme
 	case "Topic":
 		*v = new(InterfaceNoFragmentsQueryWithPointerTopic)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for InterfaceNoFragmentsQueryWithPointerContent: "%v"`, tn.TypeName)
diff --git a/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go
new file mode 100644
index 0000000..f9a9399
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go
@@ -0,0 +1,190 @@
+package test
+
+// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
+
+import (
+	"encoding/json"
+	"fmt"
+
+	"github.com/Khan/genqlient/graphql"
+	"github.com/Khan/genqlient/internal/testutil"
+)
+
+// SimpleInlineFragmentRandomItemArticle includes the requested fields of the GraphQL type Article.
+type SimpleInlineFragmentRandomItemArticle struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+	Text string      `json:"text"`
+}
+
+// SimpleInlineFragmentRandomItemContent includes the requested fields of the GraphQL interface Content.
+//
+// SimpleInlineFragmentRandomItemContent is implemented by the following types:
+// SimpleInlineFragmentRandomItemArticle
+// SimpleInlineFragmentRandomItemVideo
+// SimpleInlineFragmentRandomItemTopic
+//
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type SimpleInlineFragmentRandomItemContent interface {
+	implementsGraphQLInterfaceSimpleInlineFragmentRandomItemContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+	// GetId returns the interface-field "id" from its implementation.
+	// The GraphQL interface field's documentation follows.
+	//
+	// ID is the identifier of the content.
+	GetId() testutil.ID
+	// GetName returns the interface-field "name" from its implementation.
+	GetName() string
+}
+
+func (v *SimpleInlineFragmentRandomItemArticle) implementsGraphQLInterfaceSimpleInlineFragmentRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface SimpleInlineFragmentRandomItemContent.
+func (v *SimpleInlineFragmentRandomItemArticle) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface SimpleInlineFragmentRandomItemContent.
+func (v *SimpleInlineFragmentRandomItemArticle) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface SimpleInlineFragmentRandomItemContent.
+func (v *SimpleInlineFragmentRandomItemArticle) GetName() string { return v.Name }
+
+func (v *SimpleInlineFragmentRandomItemVideo) implementsGraphQLInterfaceSimpleInlineFragmentRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface SimpleInlineFragmentRandomItemContent.
+func (v *SimpleInlineFragmentRandomItemVideo) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface SimpleInlineFragmentRandomItemContent.
+func (v *SimpleInlineFragmentRandomItemVideo) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface SimpleInlineFragmentRandomItemContent.
+func (v *SimpleInlineFragmentRandomItemVideo) GetName() string { return v.Name }
+
+func (v *SimpleInlineFragmentRandomItemTopic) implementsGraphQLInterfaceSimpleInlineFragmentRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface SimpleInlineFragmentRandomItemContent.
+func (v *SimpleInlineFragmentRandomItemTopic) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface SimpleInlineFragmentRandomItemContent.
+func (v *SimpleInlineFragmentRandomItemTopic) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface SimpleInlineFragmentRandomItemContent.
+func (v *SimpleInlineFragmentRandomItemTopic) GetName() string { return v.Name }
+
+func __unmarshalSimpleInlineFragmentRandomItemContent(v *SimpleInlineFragmentRandomItemContent, m json.RawMessage) error {
+	if string(m) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(m, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "Article":
+		*v = new(SimpleInlineFragmentRandomItemArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(SimpleInlineFragmentRandomItemVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(SimpleInlineFragmentRandomItemTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for SimpleInlineFragmentRandomItemContent: "%v"`, tn.TypeName)
+	}
+}
+
+// SimpleInlineFragmentRandomItemTopic includes the requested fields of the GraphQL type Topic.
+type SimpleInlineFragmentRandomItemTopic struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// SimpleInlineFragmentRandomItemVideo includes the requested fields of the GraphQL type Video.
+type SimpleInlineFragmentRandomItemVideo struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id       testutil.ID `json:"id"`
+	Name     string      `json:"name"`
+	Duration int         `json:"duration"`
+}
+
+// SimpleInlineFragmentResponse is returned by SimpleInlineFragment on success.
+type SimpleInlineFragmentResponse struct {
+	RandomItem SimpleInlineFragmentRandomItemContent `json:"-"`
+}
+
+func (v *SimpleInlineFragmentResponse) UnmarshalJSON(b []byte) error {
+
+	type SimpleInlineFragmentResponseWrapper SimpleInlineFragmentResponse
+
+	var firstPass struct {
+		*SimpleInlineFragmentResponseWrapper
+		RandomItem json.RawMessage `json:"randomItem"`
+	}
+	firstPass.SimpleInlineFragmentResponseWrapper = (*SimpleInlineFragmentResponseWrapper)(v)
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.RandomItem
+		raw := firstPass.RandomItem
+		err = __unmarshalSimpleInlineFragmentRandomItemContent(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal SimpleInlineFragmentResponse.RandomItem: %w", err)
+		}
+	}
+	return nil
+}
+
+func SimpleInlineFragment(
+	client graphql.Client,
+) (*SimpleInlineFragmentResponse, error) {
+	var retval SimpleInlineFragmentResponse
+	err := client.MakeRequest(
+		nil,
+		"SimpleInlineFragment",
+		`
+query SimpleInlineFragment {
+	randomItem {
+		__typename
+		id
+		name
+		... on Article {
+			text
+		}
+		... on Video {
+			duration
+		}
+	}
+}
+`,
+		&retval,
+		nil,
+	)
+	return &retval, err
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.json b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.json
new file mode 100644
index 0000000..150cfa9
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "SimpleInlineFragment",
+      "query": "\nquery SimpleInlineFragment {\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\tname\n\t\t... on Article {\n\t\t\ttext\n\t\t}\n\t\t... on Video {\n\t\t\tduration\n\t\t}\n\t}\n}\n",
+      "sourceLocation": "testdata/queries/SimpleInlineFragment.graphql"
+    }
+  ]
+}
diff --git a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go
index 43fe75b..da7013d 100644
--- a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go
@@ -61,6 +61,9 @@ func __unmarshalUnionNoFragmentsQueryRandomLeafLeafContent(v *UnionNoFragmentsQu
 	case "Video":
 		*v = new(UnionNoFragmentsQueryRandomLeafVideo)
 		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing LeafContent.__typename")
 	default:
 		return fmt.Errorf(
 			`Unexpected concrete type for UnionNoFragmentsQueryRandomLeafLeafContent: "%v"`, tn.TypeName)
@@ -98,7 +101,8 @@ func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 		err = __unmarshalUnionNoFragmentsQueryRandomLeafLeafContent(
 			target, raw)
 		if err != nil {
-			return err
+			return fmt.Errorf(
+				"Unable to unmarshal UnionNoFragmentsQueryResponse.RandomLeaf: %w", err)
 		}
 	}
 	return nil
diff --git a/generate/testdata/snapshots/TestGenerateErrors-BindingWithIncorrectSelection.go b/generate/testdata/snapshots/TestGenerateErrors-BindingWithIncorrectSelection.go
new file mode 100644
index 0000000..6768bbf
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerateErrors-BindingWithIncorrectSelection.go
@@ -0,0 +1,1 @@
+invalid selection for type-binding GetPokemonWrongFieldsPokemon: testdata/errors/BindingWithIncorrectSelection.schema.graphql:2: expected 2 fields, got 1
diff --git a/generate/testdata/snapshots/TestGenerateErrors-BindingWithIncorrectSelection.graphql b/generate/testdata/snapshots/TestGenerateErrors-BindingWithIncorrectSelection.graphql
new file mode 100644
index 0000000..fbaaea2
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerateErrors-BindingWithIncorrectSelection.graphql
@@ -0,0 +1,1 @@
+invalid selection for type-binding GetPokemonWrongFieldsPokemon: testdata/errors/BindingWithIncorrectSelection.graphql:2: expected field 1 to be level, got species
diff --git a/generate/testdata/snapshots/TestGenerateErrors-ConflictingSelections.go b/generate/testdata/snapshots/TestGenerateErrors-ConflictingSelections.go
new file mode 100644
index 0000000..4b3033f
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerateErrors-ConflictingSelections.go
@@ -0,0 +1,1 @@
+testdata/errors/ConflictingSelections.go:4: operations must have operation-names
diff --git a/generate/testdata/snapshots/TestGenerateErrors-ConflictingSelections.graphql b/generate/testdata/snapshots/TestGenerateErrors-ConflictingSelections.graphql
new file mode 100644
index 0000000..b3bf989
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerateErrors-ConflictingSelections.graphql
@@ -0,0 +1,1 @@
+testdata/errors/ConflictingSelections.graphql:1: operations must have operation-names
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index b759ef3..de3578b 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -206,6 +206,91 @@ func TestInterfaceListPointerField(t *testing.T) {
 	assert.Nil(t, *resp.Beings[2])
 }
 
+func TestFragments(t *testing.T) {
+	_ = `# @genqlient
+	query queryWithFragments($ids: [ID!]!) {
+		beings(ids: $ids) {
+			__typename id
+			... on Being { id name }
+			... on Animal {
+				id
+				hair { hasHair }
+				species
+				owner {
+					id
+					... on Being { name }
+					... on User { luckyNumber }
+				}
+			}
+			... on Lucky { luckyNumber }
+			... on User { hair { color } }
+		}
+	}`
+
+	ctx := context.Background()
+	server := server.RunServer()
+	defer server.Close()
+	client := graphql.NewClient(server.URL, http.DefaultClient)
+
+	resp, err := queryWithFragments(ctx, client, []string{"1", "3", "12847394823"})
+	require.NoError(t, err)
+
+	require.Len(t, resp.Beings, 3)
+
+	// We should get the following three beings:
+	//	User{Id: 1, Name: "Yours Truly"},
+	//	Animal{Id: 3, Name: "Fido"},
+	//	null
+
+	// Check fields both via interface and via type-assertion when possible
+	// User has, in total, the fields: __typename id name luckyNumber.
+	assert.Equal(t, "User", resp.Beings[0].GetTypename())
+	assert.Equal(t, "1", resp.Beings[0].GetId())
+	assert.Equal(t, "Yours Truly", resp.Beings[0].GetName())
+	// (hair and luckyNumber we need to cast for)
+
+	user, ok := resp.Beings[0].(*queryWithFragmentsBeingsUser)
+	require.Truef(t, ok, "got %T, not User", resp.Beings[0])
+	assert.Equal(t, "1", user.Id)
+	assert.Equal(t, "Yours Truly", user.Name)
+	// TODO(benkraft): Uncomment once we fix the interface-field type-naming
+	// bug that's causing this to get the wrong type (because we end up
+	// generating two conflicting types).
+	//	assert.Equal(t, "Black", user.Hair.Color)
+	assert.Equal(t, 17, user.LuckyNumber)
+
+	// Animal has, in total, the fields:
+	//	__typename
+	//	id
+	//	species
+	//	owner {
+	//		id
+	//		name
+	//		... on User { luckyNumber }
+	//	}
+	assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
+	assert.Equal(t, "3", resp.Beings[1].GetId())
+	// (hair, species, and owner.* we have to cast for)
+
+	animal, ok := resp.Beings[1].(*queryWithFragmentsBeingsAnimal)
+	require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
+	assert.Equal(t, "3", animal.Id)
+	assert.Equal(t, SpeciesDog, animal.Species)
+	assert.True(t, animal.Hair.HasHair)
+
+	assert.Equal(t, "1", animal.Owner.GetId())
+	assert.Equal(t, "Yours Truly", animal.Owner.GetName())
+	// (luckyNumber we have to cast for, again)
+
+	owner, ok := animal.Owner.(*queryWithFragmentsBeingsOwnerUser)
+	require.Truef(t, ok, "got %T, not User", animal.Owner)
+	assert.Equal(t, "1", owner.Id)
+	assert.Equal(t, "Yours Truly", owner.Name)
+	assert.Equal(t, 17, owner.LuckyNumber)
+
+	assert.Nil(t, resp.Beings[2])
+}
+
 func TestGeneratedCode(t *testing.T) {
 	// TODO(benkraft): Check that gqlgen is up to date too.  In practice that's
 	// less likely to be a problem, since it should only change if you update