Repository: Khan/genqlient
Pull request #65 (2021-08-25 22:40:38)

Add support for inline fragments
## Summary:
In this commit I add support for inline fragments
(`... on MyType { fields }`) to genqlient.  This will make interfaces a
lot more useful!  In future commits I'll add named fragments, for which
we'll generate slightly different types, as discussed in DESIGN.md.

In general, implementing the flattening approach described in DESIGN.md
was... surprisingly easy.  All we have to do is recurse on applicable
fragments when generating our selection-set.  The refactor to
selection-set handling this encouraged was, I think, quite beneficial.
It did reveal two tricky pre-existing issues.

One issue is that GraphQL allows for duplicate selections, as long as
they match.  (In practice, this is only useful in the context of
fragments, although GraphQL allows it even without.) I decided to handle
the simple case (duplicate leaf fields; we just deduplicate) but leave
to the future the complex cases where we need to merge different
sub-selections (now #64).  For now we just forbid that; we can see how
much it comes up.

The other issue is that we are generating type-names incorrectly for
interface types; I had intended to do `MyInterfaceMyFieldMyType` for
shared fields and `MyImplMyFieldMyType` for non-shared ones, but instead
I did `MyFieldMyType`, which is inconsistent already and can result in
conflicts in the presence of fragments.  I'm going to fix this in a
separate commit, though, because it's going to require some refactoring
and is irrelevant to the main logic of this commit; I left some TODOs in
the tests related to this.

Issue: https://github.com/Khan/genqlient/issues/8

## Test plan:
make check



Patch:
diff --git a/generate/convert.go b/generate/convert.go
index d506615..8d47117 100644
--- a/generate/convert.go
+++ b/generate/convert.go
@@ -200,38 +200,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 +252,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 +323,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/internal/integration/generated.go b/internal/integration/generated.go
index 69e7250..8cf8333 100644
--- a/internal/integration/generated.go
+++ b/internal/integration/generated.go
@@ -10,6 +10,253 @@ 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 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)
+	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)
+	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 err
+			}
+		}
+	}
+	return nil
+}
+
 // queryWithInterfaceListFieldBeingsAnimal includes the requested fields of the GraphQL type Animal.
 type queryWithInterfaceListFieldBeingsAnimal struct {
 	Typename string `json:"__typename"`
@@ -537,3 +784,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/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/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..b223929
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go
@@ -0,0 +1,939 @@
+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)
+	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)
+	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 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)
+	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)
+	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 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)
+	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)
+	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 err
+		}
+	}
+	{
+		target := &v.RepeatedStuff
+		raw := firstPass.RepeatedStuff
+		err = __unmarshalComplexInlineFragmentsRepeatedStuffContent(
+			target, raw)
+		if err != nil {
+			return err
+		}
+	}
+	{
+		target := &v.ConflictingStuff
+		raw := firstPass.ConflictingStuff
+		err = __unmarshalComplexInlineFragmentsConflictingStuffContent(
+			target, raw)
+		if err != nil {
+			return err
+		}
+	}
+	{
+		target := &v.NestedStuff
+		raw := firstPass.NestedStuff
+		err = __unmarshalComplexInlineFragmentsNestedStuffContent(
+			target, raw)
+		if err != nil {
+			return 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-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go
new file mode 100644
index 0000000..f3e2702
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go
@@ -0,0 +1,186 @@
+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)
+	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 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/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