Repository: Khan/genqlient
Pull request #75 (2021-09-03 00:43:16)

Add support for concrete-typed named fragments
## Summary:
In previous commits I added support to genqlient for interfaces and
inline fragments.  This means the only query structures that remain are
named fragments and their spreads, e.g.
```
fragment MyFragment on MyType { myField }
query MyQuery { getMyType { ...MyFragment } }
```
Other than mere completionism, these are potentially useful for code
sharing: you can spread the same fragment multiple places; and then
genqlient can notice that and generate the same type for each.  (They
can even be shared between different queries in the same package.)

In this commit I add support for named fragments of concrete
(object/struct, not interface) type, spread into either concrete or
abstract scope.  For genqlient's purposes, these are a new "root"
type-name, just like each operation, and are then embedded into the
appropriate struct.  (Using embeds allows their fields to be referenced
as fields of the containing type, if convenient.  Further design
considerations are discussed in DESIGN.md.)

This requires new code in two main places (plus miscellaneous glue),
both nontrivial but neither particularly complex:
- We need to actually traverse both structures and generate the types
  (in `convert.go`).
- We need to decide which fragments from this package to send to the
  server, both for good hyigene and because GraphQL requires we send
  only ones this query uses (in `generate.go`).
- We need a little new wiring for options -- because fragments can be
  shared between queries they get their own toplevel options, rather
  than inheriting the query's options.

Finally, this required slightly subtler changes to how we do
unmarshaling (in `types.go` and `unmarshal.go.tmpl`).  Basically,
because embedded fields' methods, including `UnmarshalJSON`, get
promoted to the parent type, and because the JSON library ignores their
fields when shadowed by those of the parent type, we need a little bit
of special logic in each such parent type to do its own unmarshal and
then delegate to each embed.  This is similar (and much simpler) to
what we did for interfaces, although it required some changes to the
"method-hiding" trick (used for both).  It's only really necessary in
certain specific cases (namely when an embedded type has an
`UnmarshalJSON` method or a field with the same name as the embedder),
but it's easier to just generate it always.  This is all described in
more detail inline.

This does not support fragments of abstract type, which have their own
complexities.  I'll address those, which are now the only remaining
piece of #8, in a future commit.

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

## Test plan:
make check


csilvers: I want to keep track of when this lands, and I don't know how to do that without joining the thread, so I'm making this contentless comment!
benjaminjkraft: @csilvers I believe you can subscribe with the button in the sidebar -- this one (except it will be in the opposite toggle-state)
![GitHub "unsubscribe from notifications" button](https://user-images.githubusercontent.com/111238/132726555-267dc9fb-b7c9-4390-8a56-ab26a4905f16.png)


Patch:
diff --git a/generate/convert.go b/generate/convert.go
index b8ad019..7512193 100644
--- a/generate/convert.go
+++ b/generate/convert.go
@@ -330,7 +330,12 @@ func (g *generator) convertSelectionSet(
 			}
 			fields = append(fields, field)
 		case *ast.FragmentSpread:
-			return nil, errorf(selection.Position, "not implemented: %T", selection)
+			maybeField, err := g.convertFragmentSpread(selection, containingTypedef)
+			if err != nil {
+				return nil, err
+			} else if maybeField != nil {
+				fields = append(fields, maybeField)
+			}
 		case *ast.InlineFragment:
 			// (Note this will return nil, nil if the fragment doesn't apply to
 			// this type.)
@@ -354,7 +359,9 @@ func (g *generator) convertSelectionSet(
 		// 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] {
+		// It's fine to have duplicate embeds (i.e. via named fragments), even
+		// ones with complicated overlaps, since they are separate types to us.
+		if field.JSONName != "" && 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
@@ -444,6 +451,72 @@ func (g *generator) convertInlineFragment(
 		containingTypedef, queryOptions)
 }
 
+// convertFragmentSpread converts a single GraphQL fragment-spread
+// (`...MyFragment`) into a Go struct-field.  If the fragment does not apply to
+// this type, returns nil.
+//
+// containingTypedef is as described in convertInlineFragment, above.
+func (g *generator) convertFragmentSpread(
+	fragmentSpread *ast.FragmentSpread,
+	containingTypedef *ast.Definition,
+) (*goStructField, error) {
+	if !fragmentMatches(containingTypedef, fragmentSpread.Definition.Definition) {
+		return nil, nil
+	}
+
+	typ, ok := g.typeMap[fragmentSpread.Name]
+	if !ok {
+		// If we haven't yet, convert the fragment itself.  Note that fragments
+		// aren't allowed to have cycles, so this won't recurse forever.
+		var err error
+		typ, err = g.convertNamedFragment(fragmentSpread.Definition)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return &goStructField{GoName: "" /* i.e. embedded */, GoType: typ}, nil
+}
+
+// convertNamedFragment converts a single GraphQL named fragment-definition
+// (`fragment MyFragment on MyType { ... }`) into a Go struct.
+func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goType, error) {
+	typ := g.schema.Types[fragment.TypeCondition]
+	if !g.Config.AllowBrokenFeatures &&
+		(typ.Kind == ast.Interface || typ.Kind == ast.Union) {
+		return nil, errorf(fragment.Position, "not implemented: abstract-typed fragments")
+	}
+
+	description, directive, err := g.parsePrecedingComment(fragment, fragment.Position)
+	if err != nil {
+		return nil, err
+	}
+
+	// If the user included a comment, use that.  Else make up something
+	// generic; there's not much to say though.
+	if description == "" {
+		description = fmt.Sprintf(
+			"%v includes the GraphQL fields of %v requested by the fragment %v.",
+			fragment.Name, fragment.TypeCondition, fragment.Name)
+	}
+
+	fields, err := g.convertSelectionSet(
+		newPrefixList(fragment.Name), fragment.SelectionSet, typ, directive)
+	if err != nil {
+		return nil, err
+	}
+
+	goType := &goStructType{
+		GoName:      fragment.Name,
+		Description: description,
+		GraphQLName: fragment.TypeCondition,
+		Fields:      fields,
+		Incomplete:  false,
+	}
+	g.typeMap[fragment.Name] = goType
+	return goType, nil
+}
+
 // convertField converts a single GraphQL operation-field into a Go
 // struct-field (and its type).
 //
diff --git a/generate/generate.go b/generate/generate.go
index c54f29c..2011fc3 100644
--- a/generate/generate.go
+++ b/generate/generate.go
@@ -35,6 +35,11 @@ type generator struct {
 	templateCache map[string]*template.Template
 	// Schema we are generating code against
 	schema *ast.Schema
+	// Named fragments (map by name), so we can look them up from spreads.
+	// TODO(benkraft): In theory we shouldn't need this, we can just use
+	// ast.FragmentSpread.Definition, but for some reason it doesn't seem to be
+	// set consistently, even post-validation.
+	fragments map[string]*ast.FragmentDefinition
 }
 
 // JSON tags in operation are for ExportOperations (see Config for details).
@@ -67,7 +72,11 @@ type argument struct {
 	Options     *GenqlientDirective
 }
 
-func newGenerator(config *Config, schema *ast.Schema) (*generator, error) {
+func newGenerator(
+	config *Config,
+	schema *ast.Schema,
+	fragments ast.FragmentDefinitionList,
+) (*generator, error) {
 	g := generator{
 		Config:        config,
 		typeMap:       map[string]goType{},
@@ -75,6 +84,11 @@ func newGenerator(config *Config, schema *ast.Schema) (*generator, error) {
 		usedAliases:   map[string]bool{},
 		templateCache: map[string]*template.Template{},
 		schema:        schema,
+		fragments:     make(map[string]*ast.FragmentDefinition, len(fragments)),
+	}
+
+	for _, fragment := range fragments {
+		g.fragments[fragment.Name] = fragment
 	}
 
 	_, err := g.addRef("github.com/Khan/genqlient/graphql.Client")
@@ -146,6 +160,38 @@ func (g *generator) getArgument(
 	}, nil
 }
 
+// usedFragmentNames returns the named-fragments used by (i.e. spread into)
+// this operation.
+func (g *generator) usedFragments(op *ast.OperationDefinition) ast.FragmentDefinitionList {
+	var retval, queue ast.FragmentDefinitionList
+	seen := map[string]bool{}
+
+	var observers validator.Events
+	// Fragment-spreads are easy to find; just ask for them!
+	observers.OnFragmentSpread(func(_ *validator.Walker, fragmentSpread *ast.FragmentSpread) {
+		if seen[fragmentSpread.Name] {
+			return
+		}
+		def := g.fragments[fragmentSpread.Name]
+		seen[fragmentSpread.Name] = true
+		retval = append(retval, def)
+		queue = append(queue, def)
+	})
+
+	doc := ast.QueryDocument{Operations: ast.OperationList{op}}
+	validator.Walk(g.schema, &doc, &observers)
+	// Well, easy-ish: we also have to look recursively.
+	// Note GraphQL guarantees there are no cycles among fragments:
+	// https://spec.graphql.org/draft/#sec-Fragment-spreads-must-not-form-cycles
+	for len(queue) > 0 {
+		doc = ast.QueryDocument{Fragments: ast.FragmentDefinitionList{queue[0]}}
+		validator.Walk(g.schema, &doc, &observers) // traversal is the same
+		queue = queue[1:]
+	}
+
+	return retval
+}
+
 // Preprocess each query to make any changes that genqlient needs.
 //
 // At present, the only change is that we add __typename, if not already
@@ -158,9 +204,11 @@ func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) {
 	// __typename field.  There are four places we might find a selection-set:
 	// at the toplevel of a query, on a field, or in an inline or named
 	// fragment.  The toplevel of a query must be an object type, so we don't
-	// need to consider that.
-	// TODO(benkraft): Once we support fragments, figure out whether we need to
-	// traverse inline/named fragments here too.
+	// need to consider that.  And fragments must (if used at all) be spread
+	// into some parent selection-set, so we'll add __typename there (if
+	// needed).  Note this does mean abstract-typed fragments spread into
+	// object-typed scope will *not* have access to `__typename`, but they
+	// indeed don't need it, since we do know the type in that context.
 	observers.OnField(func(_ *validator.Walker, field *ast.Field) {
 		// We are interested in a field from the query like
 		//	field { subField ... }
@@ -214,6 +262,11 @@ func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) {
 	validator.Walk(g.schema, doc, &observers)
 }
 
+// addOperation adds to g.Operations the information needed to generate a
+// genqlient entrypoint function for the given operation.  It also adds to
+// g.typeMap any types referenced by the operation, except for types belonging
+// to named fragments, which are added separately by Generate via
+// convertFragment.
 func (g *generator) addOperation(op *ast.OperationDefinition) error {
 	if op.Name == "" {
 		return errorf(op.Position, "operations must have operation-names")
@@ -221,7 +274,7 @@ func (g *generator) addOperation(op *ast.OperationDefinition) error {
 
 	queryDoc := &ast.QueryDocument{
 		Operations: ast.OperationList{op},
-		// TODO: handle fragments
+		Fragments:  g.usedFragments(op),
 	}
 	g.preprocessQueryDocument(queryDoc)
 
@@ -304,19 +357,13 @@ func Generate(config *Config) (map[string][]byte, error) {
 			strings.Join(config.Operations, ", "))
 	}
 
-	if len(document.Fragments) > 0 && !config.AllowBrokenFeatures {
-		return nil, errorf(document.Fragments[0].Position,
-			"genqlient does not yet support fragments")
-	}
-
-	// Step 2: For each operation, convert it into data structures representing
-	// Go types (defined in types.go).  The bulk of this logic is in
-	// convert.go.
-	g, err := newGenerator(config, schema)
+	// Step 2: For each operation and fragment, convert it into data structures
+	// representing Go types (defined in types.go).  The bulk of this logic is
+	// in convert.go.
+	g, err := newGenerator(config, schema, document.Fragments)
 	if err != nil {
 		return nil, err
 	}
-
 	for _, op := range document.Operations {
 		if err = g.addOperation(op); err != nil {
 			return nil, err
diff --git a/generate/genqlient_directive.go b/generate/genqlient_directive.go
index 5abf8b4..99f68cf 100644
--- a/generate/genqlient_directive.go
+++ b/generate/genqlient_directive.go
@@ -153,6 +153,15 @@ func (dir *GenqlientDirective) validate(node interface{}) error {
 		// Anything else is valid on the entire operation; it will just apply
 		// to whatever it is relevant to.
 		return nil
+	case *ast.FragmentDefinition:
+		if dir.Bind != "" {
+			// TODO(benkraft): Implement this if people find it useful.
+			return errorf(dir.pos, "bind is not implemented for named fragments")
+		}
+
+		// Like operations, anything else will just apply to the entire
+		// fragment.
+		return nil
 	case *ast.VariableDefinition:
 		if dir.Omitempty != nil && node.Type.NonNull {
 			return errorf(dir.pos, "omitempty may only be used on optional arguments")
@@ -164,7 +173,7 @@ func (dir *GenqlientDirective) validate(node interface{}) error {
 		}
 		return nil
 	default:
-		return errorf(dir.pos, "invalid directive location: %T", node)
+		return errorf(dir.pos, "invalid @genqlient directive location: %T", node)
 	}
 }
 
diff --git a/generate/types.go b/generate/types.go
index f978385..4ffbd10 100644
--- a/generate/types.go
+++ b/generate/types.go
@@ -119,11 +119,19 @@ type goStructField struct {
 	Description string
 }
 
-func isAbstract(typ goType) bool {
-	_, ok := typ.Unwrap().(*goInterfaceType)
+// IsAbstract returns true if this field is of abstract type (i.e. GraphQL
+// union or interface; equivalently, represented by an interface in Go).
+func (field *goStructField) IsAbstract() bool {
+	_, ok := field.GoType.Unwrap().(*goInterfaceType)
 	return ok
 }
 
+// IsEmbedded returns true if this field is embedded (a.k.a. anonymous), which
+// is in practice true if it corresponds to a named fragment spread in GraphQL.
+func (field *goStructField) IsEmbedded() bool {
+	return field.GoName == ""
+}
+
 func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
 	description := typ.Description
 	if typ.Incomplete {
@@ -144,28 +152,47 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
 	}
 	writeDescription(w, description)
 
+	needUnmarshaler := false
 	fmt.Fprintf(w, "type %s struct {\n", typ.GoName)
 	for _, field := range typ.Fields {
 		writeDescription(w, field.Description)
 		jsonName := field.JSONName
-		if isAbstract(field.GoType) {
-			// abstract types are handled in our UnmarshalJSON
+		if field.IsAbstract() {
+			// abstract types are handled in our UnmarshalJSON (see below)
+			needUnmarshaler = true
 			jsonName = "-"
 		}
-		fmt.Fprintf(w, "\t%s %s `json:\"%s\"`\n",
-			field.GoName, field.GoType.Reference(), jsonName)
+		if field.IsEmbedded() {
+			// embedded fields also need UnmarshalJSON handling (see below)
+			needUnmarshaler = true
+			fmt.Fprintf(w, "\t%s `json:\"-\"`\n", field.GoType.Unwrap().Reference())
+		} else {
+			fmt.Fprintf(w, "\t%s %s `json:\"%s\"`\n",
+				field.GoName, field.GoType.Reference(), jsonName)
+		}
 	}
 	fmt.Fprintf(w, "}\n")
 
-	// Now, if needed, write the unmarshaler.
+	// Now, if needed, write the unmarshaler.  We need one if we have any
+	// interface-typed fields, or any embedded fields.
+	//
+	// For interface-typed fields, ideally we'd write an UnmarshalJSON method
+	// on the field, but you can't add a method to an interface.  So we write a
+	// per-interface-type helper, but we have to call it (with a little
+	// boilerplate) everywhere the type is referenced.
 	//
-	// Specifically, in order to unmarshal interface values, we need to add an
-	// UnmarshalJSON method to each type which has an interface-typed *field*
-	// (not the interface type itself -- we can't add methods to that).
-	// But we put most of the logic in a per-interface-type helper function,
-	// written along with the interface type; the UnmarshalJSON method is just
-	// the boilerplate.
-	if len(typ.AbstractFields()) == 0 {
+	// For embedded fields (from fragments), mostly the JSON library would just
+	// do what we want, but there are two problems.  First, if the embedded
+	// type has its own UnmarshalJSON, naively that would be promoted to
+	// become our UnmarshalJSON, which is no good.  But we don't want to just
+	// hide that method and inline its fields, either; we need to call its
+	// UnmarshalJSON (on the same object we unmarshal into this struct).
+	// Second, if the embedded type duplicates any fields of the embedding type
+	// -- maybe both the fragment and the selection into which it's spread
+	// select the same field, or several fragments select the same field -- the
+	// JSON library will only fill one of those (the least-nested one); we want
+	// to fill them all.
+	if !needUnmarshaler {
 		return nil
 	}
 
@@ -181,18 +208,6 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
 
 func (typ *goStructType) Reference() string { return typ.GoName }
 
-// AbstractFields returns all the fields which are abstract types (i.e. GraphQL
-// unions and interfaces; equivalently, types represented by interfaces in Go).
-func (typ *goStructType) AbstractFields() []*goStructField {
-	var ret []*goStructField
-	for _, field := range typ.Fields {
-		if isAbstract(field.GoType) {
-			ret = append(ret, field)
-		}
-	}
-	return ret
-}
-
 // goInterfaceType represents a Go interface type, used to represent a GraphQL
 // interface or union type.
 type goInterfaceType struct {
diff --git a/generate/unmarshal.go.tmpl b/generate/unmarshal.go.tmpl
index d91a7f6..fa99cf3 100644
--- a/generate/unmarshal.go.tmpl
+++ b/generate/unmarshal.go.tmpl
@@ -2,36 +2,49 @@
      UnmarshalJSON from the function it follows) */}}
 
 func (v *{{.GoName}}) UnmarshalJSON(b []byte) error {
-    {{/* We want to specially handle the abstract fields (.AbstractFields),
-         but unmarshal everything else normally.  To handle abstract fields,
+    {{/* We want to specially handle the abstract or embedded fields, but
+         unmarshal everything else normally.  To handle abstract fields,
          first we unmarshal them into a json.RawMessage, and then handle those
-         further, below.  For the rest, we just want to call json.Unmarshal.
+         further, below.  Embedded fields we just unmarshal directly into the
+         embedded value.  For the rest, we just want to call json.Unmarshal.
          But if we do that naively on a value of type `.Type`, it will call
          this function again, and recurse infinitely.  So we make a wrapper
-         type -- with a different name, thus different methods, but the same
-         fields, and unmarshal into that.  For more on why this is so
-         difficult, see
-         https://github.com/benjaminjkraft/notes/blob/master/go-json-interfaces.md
+         type which embeds both this type and NoUmnarshalJSON, which prevents
+         either's UnmarshalJSON method from being promoted.  For more on why
+         this is so difficult, see
+         https://github.com/benjaminjkraft/notes/blob/master/go-json-interfaces.md.
+         (Note there are a few different ways "hide" the method, but this one
+         seems to be the best option that works if this type has embedded types
+         with UnmarshalJSON methods.)
          TODO(benkraft)): Ensure `{{.Type}}Wrapper` won't collide with any
          other type we need.  (For the most part it being locally-scoped saves
          us; it's not clear if this can be a problem in practice.)
     */}}
-    type {{.GoName}}Wrapper {{.GoName}}
 
+    {{/* TODO(benkraft): Omit/simplify the first pass if all fields are
+         embedded/abstract. */ -}}
     var firstPass struct{
-        *{{.GoName}}Wrapper
-        {{range .AbstractFields -}}
+        *{{.GoName}}
+        {{range .Fields -}}
+        {{if .IsAbstract -}}
         {{.GoName}} {{repeat .GoType.SliceDepth "[]"}}{{ref "encoding/json.RawMessage"}} `json:"{{.JSONName}}"`
-        {{end}}
+        {{end -}}
+        {{end -}}
+        {{/* TODO(benkraft): In principle you might have a field-name that
+             conflicts with this one; avoid that. */ -}}
+        {{ref "github.com/Khan/genqlient/graphql.NoUnmarshalJSON"}}
     }
-    firstPass.{{.GoName}}Wrapper = (*{{.GoName}}Wrapper)(v)
+    firstPass.{{.GoName}} = v
 
     err := {{ref "encoding/json.Unmarshal"}}(b, &firstPass)
     if err != nil {
         return err
     }
 
-    {{/* Now, for each field, call out to the unmarshal-helper.
+    {{/* Now, handle the fields needing special handling. */}}
+    {{range $field := .Fields -}}
+    {{if $field.IsAbstract -}}
+    {{/* First, for abstract fields, call the unmarshal-helper.
          This gets a little complicated because we may have a slice field.
          So what we do is basically, for each field of type `[][]...[]MyType`:
 
@@ -65,7 +78,6 @@ func (v *{{.GoName}}) UnmarshalJSON(b []byte) error {
          each field we are handling, which would otherwise conflict.  (We could
          instead suffix the names, but that makes things much harder to read.)
     */}}
-    {{range $field := .AbstractFields -}}
     {
         target := &v.{{$field.GoName}}
         raw := firstPass.{{$field.GoName}}
@@ -92,7 +104,17 @@ func (v *{{.GoName}}) UnmarshalJSON(b []byte) error {
         }
         {{end -}}
     }
-    {{end -}}
+    {{end -}}{{/* end if .IsAbstract */}}
+    {{if $field.IsEmbedded -}}
+    {{/* Embedded fields are easier: we just unmarshal the same input into
+         them.  (They're also easier because they can't be lists, since they
+         arise from GraphQL fragment spreads.) */}}
+    err = json.Unmarshal(b, &v.{{$field.GoType.Unwrap.Reference}})
+    if err != nil {
+        return err
+    }
+    {{end}}{{/* end if .IsEmbedded */ -}}
+    {{end}}{{/* end range .Fields */ -}}
 
     return nil
 }
diff --git a/go.sum b/go.sum
index 72df9cc..f7c511b 100644
--- a/go.sum
+++ b/go.sum
@@ -10,6 +10,7 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
 github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
 github.com/bradleyjkemp/cupaloy/v2 v2.6.0 h1:knToPYa2xtfg42U3I6punFEjaGFKWQRXJwj0JTv4mTs=
 github.com/bradleyjkemp/cupaloy/v2 v2.6.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=
+github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -39,6 +40,7 @@ github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047 h1:zCoDWFD5
 github.com/mitchellh/mapstructure v0.0.0-20180203102830-a4e142e9c047/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74=
 github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
diff --git a/graphql/util.go b/graphql/util.go
new file mode 100644
index 0000000..b5b4564
--- /dev/null
+++ b/graphql/util.go
@@ -0,0 +1,19 @@
+package graphql
+
+// Utility types used by the generated code.  In general, these are *not*
+// intended for end-users.
+
+// NoUnmarshalJSON is intended for the use of genqlient's generated code only.
+//
+// It is used to prevent a struct type from inheriting its embed's
+// UnmarshalJSON method: given a type
+//	type T struct { E; NoUnmarshalJSON }
+// where E has an UnmarshalJSON method, T will not inherit it, per the Go
+// selector rules: https://golang.org/ref/spec#Selectors.
+type NoUnmarshalJSON struct{}
+
+// UnmarshalJSON should never be called; it exists only to prevent a sibling
+// UnmarshalJSON method from being promoted.
+func (NoUnmarshalJSON) UnmarshalJSON(b []byte) error {
+	panic("NoUnmarshalJSON.UnmarshalJSON should never be called!")
+}
diff --git a/internal/integration/generated.go b/internal/integration/generated.go
index bbc3b73..eb234ec 100644
--- a/internal/integration/generated.go
+++ b/internal/integration/generated.go
@@ -10,6 +10,152 @@ import (
 	"github.com/Khan/genqlient/graphql"
 )
 
+// AnimalFields includes the GraphQL fields of Animal requested by the fragment AnimalFields.
+type AnimalFields struct {
+	Id    string                     `json:"id"`
+	Hair  AnimalFieldsHairBeingsHair `json:"hair"`
+	Owner AnimalFieldsOwnerBeing     `json:"-"`
+}
+
+func (v *AnimalFields) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*AnimalFields
+		Owner json.RawMessage `json:"owner"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.AnimalFields = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.Owner
+		raw := firstPass.Owner
+		err = __unmarshalAnimalFieldsOwnerBeing(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal AnimalFields.Owner: %w", err)
+		}
+	}
+
+	return nil
+}
+
+// AnimalFieldsHairBeingsHair includes the requested fields of the GraphQL type BeingsHair.
+type AnimalFieldsHairBeingsHair struct {
+	HasHair bool `json:"hasHair"`
+}
+
+// AnimalFieldsOwnerAnimal includes the requested fields of the GraphQL type Animal.
+type AnimalFieldsOwnerAnimal struct {
+	Typename string `json:"__typename"`
+	Id       string `json:"id"`
+}
+
+// AnimalFieldsOwnerBeing includes the requested fields of the GraphQL interface Being.
+//
+// AnimalFieldsOwnerBeing is implemented by the following types:
+// AnimalFieldsOwnerUser
+// AnimalFieldsOwnerAnimal
+//
+// The GraphQL type's documentation follows.
+//
+//
+type AnimalFieldsOwnerBeing interface {
+	implementsGraphQLInterfaceAnimalFieldsOwnerBeing()
+	// 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
+}
+
+func (v *AnimalFieldsOwnerUser) implementsGraphQLInterfaceAnimalFieldsOwnerBeing() {}
+
+// GetTypename is a part of, and documented with, the interface AnimalFieldsOwnerBeing.
+func (v *AnimalFieldsOwnerUser) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface AnimalFieldsOwnerBeing.
+func (v *AnimalFieldsOwnerUser) GetId() string { return v.Id }
+
+func (v *AnimalFieldsOwnerAnimal) implementsGraphQLInterfaceAnimalFieldsOwnerBeing() {}
+
+// GetTypename is a part of, and documented with, the interface AnimalFieldsOwnerBeing.
+func (v *AnimalFieldsOwnerAnimal) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface AnimalFieldsOwnerBeing.
+func (v *AnimalFieldsOwnerAnimal) GetId() string { return v.Id }
+
+func __unmarshalAnimalFieldsOwnerBeing(v *AnimalFieldsOwnerBeing, 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(AnimalFieldsOwnerUser)
+		return json.Unmarshal(m, *v)
+	case "Animal":
+		*v = new(AnimalFieldsOwnerAnimal)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Being.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for AnimalFieldsOwnerBeing: "%v"`, tn.TypeName)
+	}
+}
+
+// AnimalFieldsOwnerUser includes the requested fields of the GraphQL type User.
+type AnimalFieldsOwnerUser struct {
+	Typename   string `json:"__typename"`
+	Id         string `json:"id"`
+	UserFields `json:"-"`
+}
+
+func (v *AnimalFieldsOwnerUser) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*AnimalFieldsOwnerUser
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.AnimalFieldsOwnerUser = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.UserFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// MoreUserFields includes the GraphQL fields of User requested by the fragment MoreUserFields.
+type MoreUserFields struct {
+	Id   string             `json:"id"`
+	Hair MoreUserFieldsHair `json:"hair"`
+}
+
+// MoreUserFieldsHair includes the requested fields of the GraphQL type Hair.
+type MoreUserFieldsHair struct {
+	Color string `json:"color"`
+}
+
 type Species string
 
 const (
@@ -17,6 +163,33 @@ const (
 	SpeciesCoelacanth Species = "COELACANTH"
 )
 
+// UserFields includes the GraphQL fields of User requested by the fragment UserFields.
+type UserFields struct {
+	Id             string `json:"id"`
+	LuckyNumber    int    `json:"luckyNumber"`
+	MoreUserFields `json:"-"`
+}
+
+func (v *UserFields) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*UserFields
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.UserFields = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.MoreUserFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
 // queryWithFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal.
 type queryWithFragmentsBeingsAnimal struct {
 	Typename string                                       `json:"__typename"`
@@ -29,13 +202,12 @@ type queryWithFragmentsBeingsAnimal struct {
 
 func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
 
-	type queryWithFragmentsBeingsAnimalWrapper queryWithFragmentsBeingsAnimal
-
 	var firstPass struct {
-		*queryWithFragmentsBeingsAnimalWrapper
+		*queryWithFragmentsBeingsAnimal
 		Owner json.RawMessage `json:"owner"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.queryWithFragmentsBeingsAnimalWrapper = (*queryWithFragmentsBeingsAnimalWrapper)(v)
+	firstPass.queryWithFragmentsBeingsAnimal = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -52,6 +224,7 @@ func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal queryWithFragmentsBeingsAnimal.Owner: %w", err)
 		}
 	}
+
 	return nil
 }
 
@@ -238,13 +411,12 @@ type queryWithFragmentsResponse struct {
 
 func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error {
 
-	type queryWithFragmentsResponseWrapper queryWithFragmentsResponse
-
 	var firstPass struct {
-		*queryWithFragmentsResponseWrapper
+		*queryWithFragmentsResponse
 		Beings []json.RawMessage `json:"beings"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.queryWithFragmentsResponseWrapper = (*queryWithFragmentsResponseWrapper)(v)
+	firstPass.queryWithFragmentsResponse = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -267,6 +439,7 @@ func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error {
 			}
 		}
 	}
+
 	return nil
 }
 
@@ -363,13 +536,12 @@ type queryWithInterfaceListFieldResponse struct {
 
 func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error {
 
-	type queryWithInterfaceListFieldResponseWrapper queryWithInterfaceListFieldResponse
-
 	var firstPass struct {
-		*queryWithInterfaceListFieldResponseWrapper
+		*queryWithInterfaceListFieldResponse
 		Beings []json.RawMessage `json:"beings"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.queryWithInterfaceListFieldResponseWrapper = (*queryWithInterfaceListFieldResponseWrapper)(v)
+	firstPass.queryWithInterfaceListFieldResponse = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -392,6 +564,7 @@ func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error {
 			}
 		}
 	}
+
 	return nil
 }
 
@@ -488,13 +661,12 @@ type queryWithInterfaceListPointerFieldResponse struct {
 
 func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) error {
 
-	type queryWithInterfaceListPointerFieldResponseWrapper queryWithInterfaceListPointerFieldResponse
-
 	var firstPass struct {
-		*queryWithInterfaceListPointerFieldResponseWrapper
+		*queryWithInterfaceListPointerFieldResponse
 		Beings []json.RawMessage `json:"beings"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.queryWithInterfaceListPointerFieldResponseWrapper = (*queryWithInterfaceListPointerFieldResponseWrapper)(v)
+	firstPass.queryWithInterfaceListPointerFieldResponse = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -518,6 +690,7 @@ func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) err
 			}
 		}
 	}
+
 	return nil
 }
 
@@ -621,13 +794,12 @@ type queryWithInterfaceNoFragmentsResponse struct {
 
 func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error {
 
-	type queryWithInterfaceNoFragmentsResponseWrapper queryWithInterfaceNoFragmentsResponse
-
 	var firstPass struct {
-		*queryWithInterfaceNoFragmentsResponseWrapper
+		*queryWithInterfaceNoFragmentsResponse
 		Being json.RawMessage `json:"being"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.queryWithInterfaceNoFragmentsResponseWrapper = (*queryWithInterfaceNoFragmentsResponseWrapper)(v)
+	firstPass.queryWithInterfaceNoFragmentsResponse = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -644,6 +816,164 @@ func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal queryWithInterfaceNoFragmentsResponse.Being: %w", err)
 		}
 	}
+
+	return nil
+}
+
+// queryWithNamedFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal.
+type queryWithNamedFragmentsBeingsAnimal struct {
+	Typename     string `json:"__typename"`
+	Id           string `json:"id"`
+	AnimalFields `json:"-"`
+}
+
+func (v *queryWithNamedFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*queryWithNamedFragmentsBeingsAnimal
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.queryWithNamedFragmentsBeingsAnimal = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.AnimalFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// queryWithNamedFragmentsBeingsBeing includes the requested fields of the GraphQL interface Being.
+//
+// queryWithNamedFragmentsBeingsBeing is implemented by the following types:
+// queryWithNamedFragmentsBeingsUser
+// queryWithNamedFragmentsBeingsAnimal
+//
+// The GraphQL type's documentation follows.
+//
+//
+type queryWithNamedFragmentsBeingsBeing interface {
+	implementsGraphQLInterfacequeryWithNamedFragmentsBeingsBeing()
+	// 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
+}
+
+func (v *queryWithNamedFragmentsBeingsUser) implementsGraphQLInterfacequeryWithNamedFragmentsBeingsBeing() {
+}
+
+// GetTypename is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing.
+func (v *queryWithNamedFragmentsBeingsUser) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing.
+func (v *queryWithNamedFragmentsBeingsUser) GetId() string { return v.Id }
+
+func (v *queryWithNamedFragmentsBeingsAnimal) implementsGraphQLInterfacequeryWithNamedFragmentsBeingsBeing() {
+}
+
+// GetTypename is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing.
+func (v *queryWithNamedFragmentsBeingsAnimal) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing.
+func (v *queryWithNamedFragmentsBeingsAnimal) GetId() string { return v.Id }
+
+func __unmarshalqueryWithNamedFragmentsBeingsBeing(v *queryWithNamedFragmentsBeingsBeing, 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(queryWithNamedFragmentsBeingsUser)
+		return json.Unmarshal(m, *v)
+	case "Animal":
+		*v = new(queryWithNamedFragmentsBeingsAnimal)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Being.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for queryWithNamedFragmentsBeingsBeing: "%v"`, tn.TypeName)
+	}
+}
+
+// queryWithNamedFragmentsBeingsUser includes the requested fields of the GraphQL type User.
+type queryWithNamedFragmentsBeingsUser struct {
+	Typename   string `json:"__typename"`
+	Id         string `json:"id"`
+	UserFields `json:"-"`
+}
+
+func (v *queryWithNamedFragmentsBeingsUser) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*queryWithNamedFragmentsBeingsUser
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.queryWithNamedFragmentsBeingsUser = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.UserFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// queryWithNamedFragmentsResponse is returned by queryWithNamedFragments on success.
+type queryWithNamedFragmentsResponse struct {
+	Beings []queryWithNamedFragmentsBeingsBeing `json:"-"`
+}
+
+func (v *queryWithNamedFragmentsResponse) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*queryWithNamedFragmentsResponse
+		Beings []json.RawMessage `json:"beings"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.queryWithNamedFragmentsResponse = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.Beings
+		raw := firstPass.Beings
+		*target = make(
+			[]queryWithNamedFragmentsBeingsBeing,
+			len(raw))
+		for i, raw := range raw {
+			target := &(*target)[i]
+			err = __unmarshalqueryWithNamedFragmentsBeingsBeing(
+				target, raw)
+			if err != nil {
+				return fmt.Errorf(
+					"Unable to unmarshal queryWithNamedFragmentsResponse.Beings: %w", err)
+			}
+		}
+	}
+
 	return nil
 }
 
@@ -877,3 +1207,56 @@ query queryWithFragments ($ids: [ID!]!) {
 	)
 	return &retval, err
 }
+
+func queryWithNamedFragments(
+	ctx context.Context,
+	client graphql.Client,
+	ids []string,
+) (*queryWithNamedFragmentsResponse, error) {
+	variables := map[string]interface{}{
+		"ids": ids,
+	}
+
+	var err error
+
+	var retval queryWithNamedFragmentsResponse
+	err = client.MakeRequest(
+		ctx,
+		"queryWithNamedFragments",
+		`
+query queryWithNamedFragments ($ids: [ID!]!) {
+	beings(ids: $ids) {
+		__typename
+		id
+		... AnimalFields
+		... UserFields
+	}
+}
+fragment AnimalFields on Animal {
+	id
+	hair {
+		hasHair
+	}
+	owner {
+		__typename
+		id
+		... UserFields
+	}
+}
+fragment UserFields on User {
+	id
+	luckyNumber
+	... MoreUserFields
+}
+fragment MoreUserFields on User {
+	id
+	hair {
+		color
+	}
+}
+`,
+		&retval,
+		variables,
+	)
+	return &retval, err
+}


Test patch:
diff --git a/generate/testdata/queries/ComplexNamedFragments.graphql b/generate/testdata/queries/ComplexNamedFragments.graphql
new file mode 100644
index 0000000..e837fc6
--- /dev/null
+++ b/generate/testdata/queries/ComplexNamedFragments.graphql
@@ -0,0 +1,39 @@
+fragment QueryFragment on Query {
+  ...InnerQueryFragment
+}
+
+fragment InnerQueryFragment on Query {
+  randomItem {
+    id name
+    ...VideoFields
+  }
+  randomLeaf {
+    ...VideoFields
+    ...MoreVideoFields
+  }
+  otherLeaf: randomLeaf {
+    ... on Video {
+      ...MoreVideoFields
+    }
+  }
+}
+
+fragment VideoFields on Video {
+  id name url duration thumbnail { id }
+}
+
+# @genqlient(pointer: true)
+fragment MoreVideoFields on Video {
+  id
+  parent {
+    name url
+    # @genqlient(pointer: false)
+    children {
+      ...VideoFields
+    }
+  }
+}
+
+query ComplexNamedFragments {
+  ... on Query { ...QueryFragment }
+}
diff --git a/generate/testdata/queries/SimpleNamedFragment.graphql b/generate/testdata/queries/SimpleNamedFragment.graphql
new file mode 100644
index 0000000..01d3370
--- /dev/null
+++ b/generate/testdata/queries/SimpleNamedFragment.graphql
@@ -0,0 +1,13 @@
+fragment VideoFields on Video {
+  id name url duration thumbnail { id }
+}
+
+query SimpleNamedFragment {
+  randomItem {
+    id name
+    ...VideoFields
+  }
+  randomLeaf {
+    ...VideoFields
+  }
+}
diff --git a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go
index 7ba7137..2e06b35 100644
--- a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go
@@ -184,13 +184,12 @@ type ComplexInlineFragmentsNestedStuffTopic struct {
 
 func (v *ComplexInlineFragmentsNestedStuffTopic) UnmarshalJSON(b []byte) error {
 
-	type ComplexInlineFragmentsNestedStuffTopicWrapper ComplexInlineFragmentsNestedStuffTopic
-
 	var firstPass struct {
-		*ComplexInlineFragmentsNestedStuffTopicWrapper
+		*ComplexInlineFragmentsNestedStuffTopic
 		Children []json.RawMessage `json:"children"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.ComplexInlineFragmentsNestedStuffTopicWrapper = (*ComplexInlineFragmentsNestedStuffTopicWrapper)(v)
+	firstPass.ComplexInlineFragmentsNestedStuffTopic = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -213,6 +212,7 @@ func (v *ComplexInlineFragmentsNestedStuffTopic) UnmarshalJSON(b []byte) error {
 			}
 		}
 	}
+
 	return nil
 }
 
@@ -232,13 +232,12 @@ type ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTop
 
 func (v *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic) UnmarshalJSON(b []byte) error {
 
-	type ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicWrapper ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic
-
 	var firstPass struct {
-		*ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicWrapper
+		*ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic
 		Children []json.RawMessage `json:"children"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicWrapper = (*ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicWrapper)(v)
+	firstPass.ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -261,6 +260,7 @@ func (v *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParen
 			}
 		}
 	}
+
 	return nil
 }
 
@@ -797,16 +797,15 @@ type ComplexInlineFragmentsResponse struct {
 
 func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
 
-	type ComplexInlineFragmentsResponseWrapper ComplexInlineFragmentsResponse
-
 	var firstPass struct {
-		*ComplexInlineFragmentsResponseWrapper
+		*ComplexInlineFragmentsResponse
 		RandomItem       json.RawMessage `json:"randomItem"`
 		RepeatedStuff    json.RawMessage `json:"repeatedStuff"`
 		ConflictingStuff json.RawMessage `json:"conflictingStuff"`
 		NestedStuff      json.RawMessage `json:"nestedStuff"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.ComplexInlineFragmentsResponseWrapper = (*ComplexInlineFragmentsResponseWrapper)(v)
+	firstPass.ComplexInlineFragmentsResponse = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -823,6 +822,7 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal ComplexInlineFragmentsResponse.RandomItem: %w", err)
 		}
 	}
+
 	{
 		target := &v.RepeatedStuff
 		raw := firstPass.RepeatedStuff
@@ -833,6 +833,7 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal ComplexInlineFragmentsResponse.RepeatedStuff: %w", err)
 		}
 	}
+
 	{
 		target := &v.ConflictingStuff
 		raw := firstPass.ConflictingStuff
@@ -843,6 +844,7 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal ComplexInlineFragmentsResponse.ConflictingStuff: %w", err)
 		}
 	}
+
 	{
 		target := &v.NestedStuff
 		raw := firstPass.NestedStuff
@@ -853,6 +855,7 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal ComplexInlineFragmentsResponse.NestedStuff: %w", err)
 		}
 	}
+
 	return nil
 }
 
diff --git a/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go
new file mode 100644
index 0000000..bc4bf69
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go
@@ -0,0 +1,666 @@
+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"
+)
+
+// ComplexNamedFragmentsResponse is returned by ComplexNamedFragments on success.
+type ComplexNamedFragmentsResponse struct {
+	QueryFragment `json:"-"`
+}
+
+func (v *ComplexNamedFragmentsResponse) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*ComplexNamedFragmentsResponse
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.ComplexNamedFragmentsResponse = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.QueryFragment)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// InnerQueryFragment includes the GraphQL fields of Query requested by the fragment InnerQueryFragment.
+type InnerQueryFragment struct {
+	RandomItem InnerQueryFragmentRandomItemContent     `json:"-"`
+	RandomLeaf InnerQueryFragmentRandomLeafLeafContent `json:"-"`
+	OtherLeaf  InnerQueryFragmentOtherLeafLeafContent  `json:"-"`
+}
+
+func (v *InnerQueryFragment) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*InnerQueryFragment
+		RandomItem json.RawMessage `json:"randomItem"`
+		RandomLeaf json.RawMessage `json:"randomLeaf"`
+		OtherLeaf  json.RawMessage `json:"otherLeaf"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.InnerQueryFragment = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.RandomItem
+		raw := firstPass.RandomItem
+		err = __unmarshalInnerQueryFragmentRandomItemContent(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal InnerQueryFragment.RandomItem: %w", err)
+		}
+	}
+
+	{
+		target := &v.RandomLeaf
+		raw := firstPass.RandomLeaf
+		err = __unmarshalInnerQueryFragmentRandomLeafLeafContent(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal InnerQueryFragment.RandomLeaf: %w", err)
+		}
+	}
+
+	{
+		target := &v.OtherLeaf
+		raw := firstPass.OtherLeaf
+		err = __unmarshalInnerQueryFragmentOtherLeafLeafContent(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal InnerQueryFragment.OtherLeaf: %w", err)
+		}
+	}
+
+	return nil
+}
+
+// InnerQueryFragmentOtherLeafArticle includes the requested fields of the GraphQL type Article.
+type InnerQueryFragmentOtherLeafArticle struct {
+	Typename string `json:"__typename"`
+}
+
+// InnerQueryFragmentOtherLeafLeafContent includes the requested fields of the GraphQL interface LeafContent.
+//
+// InnerQueryFragmentOtherLeafLeafContent is implemented by the following types:
+// InnerQueryFragmentOtherLeafArticle
+// InnerQueryFragmentOtherLeafVideo
+//
+// The GraphQL type's documentation follows.
+//
+// LeafContent represents content items that can't have child-nodes.
+type InnerQueryFragmentOtherLeafLeafContent interface {
+	implementsGraphQLInterfaceInnerQueryFragmentOtherLeafLeafContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+}
+
+func (v *InnerQueryFragmentOtherLeafArticle) implementsGraphQLInterfaceInnerQueryFragmentOtherLeafLeafContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface InnerQueryFragmentOtherLeafLeafContent.
+func (v *InnerQueryFragmentOtherLeafArticle) GetTypename() string { return v.Typename }
+
+func (v *InnerQueryFragmentOtherLeafVideo) implementsGraphQLInterfaceInnerQueryFragmentOtherLeafLeafContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface InnerQueryFragmentOtherLeafLeafContent.
+func (v *InnerQueryFragmentOtherLeafVideo) GetTypename() string { return v.Typename }
+
+func __unmarshalInnerQueryFragmentOtherLeafLeafContent(v *InnerQueryFragmentOtherLeafLeafContent, 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(InnerQueryFragmentOtherLeafArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(InnerQueryFragmentOtherLeafVideo)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing LeafContent.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for InnerQueryFragmentOtherLeafLeafContent: "%v"`, tn.TypeName)
+	}
+}
+
+// InnerQueryFragmentOtherLeafVideo includes the requested fields of the GraphQL type Video.
+type InnerQueryFragmentOtherLeafVideo struct {
+	Typename        string `json:"__typename"`
+	MoreVideoFields `json:"-"`
+}
+
+func (v *InnerQueryFragmentOtherLeafVideo) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*InnerQueryFragmentOtherLeafVideo
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.InnerQueryFragmentOtherLeafVideo = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.MoreVideoFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// InnerQueryFragmentRandomItemArticle includes the requested fields of the GraphQL type Article.
+type InnerQueryFragmentRandomItemArticle struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// InnerQueryFragmentRandomItemContent includes the requested fields of the GraphQL interface Content.
+//
+// InnerQueryFragmentRandomItemContent is implemented by the following types:
+// InnerQueryFragmentRandomItemArticle
+// InnerQueryFragmentRandomItemVideo
+// InnerQueryFragmentRandomItemTopic
+//
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type InnerQueryFragmentRandomItemContent interface {
+	implementsGraphQLInterfaceInnerQueryFragmentRandomItemContent()
+	// 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 *InnerQueryFragmentRandomItemArticle) implementsGraphQLInterfaceInnerQueryFragmentRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent.
+func (v *InnerQueryFragmentRandomItemArticle) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent.
+func (v *InnerQueryFragmentRandomItemArticle) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent.
+func (v *InnerQueryFragmentRandomItemArticle) GetName() string { return v.Name }
+
+func (v *InnerQueryFragmentRandomItemVideo) implementsGraphQLInterfaceInnerQueryFragmentRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent.
+func (v *InnerQueryFragmentRandomItemVideo) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent.
+func (v *InnerQueryFragmentRandomItemVideo) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent.
+func (v *InnerQueryFragmentRandomItemVideo) GetName() string { return v.Name }
+
+func (v *InnerQueryFragmentRandomItemTopic) implementsGraphQLInterfaceInnerQueryFragmentRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent.
+func (v *InnerQueryFragmentRandomItemTopic) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent.
+func (v *InnerQueryFragmentRandomItemTopic) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent.
+func (v *InnerQueryFragmentRandomItemTopic) GetName() string { return v.Name }
+
+func __unmarshalInnerQueryFragmentRandomItemContent(v *InnerQueryFragmentRandomItemContent, 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(InnerQueryFragmentRandomItemArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(InnerQueryFragmentRandomItemVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(InnerQueryFragmentRandomItemTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for InnerQueryFragmentRandomItemContent: "%v"`, tn.TypeName)
+	}
+}
+
+// InnerQueryFragmentRandomItemTopic includes the requested fields of the GraphQL type Topic.
+type InnerQueryFragmentRandomItemTopic struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// InnerQueryFragmentRandomItemVideo includes the requested fields of the GraphQL type Video.
+type InnerQueryFragmentRandomItemVideo struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id          testutil.ID `json:"id"`
+	Name        string      `json:"name"`
+	VideoFields `json:"-"`
+}
+
+func (v *InnerQueryFragmentRandomItemVideo) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*InnerQueryFragmentRandomItemVideo
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.InnerQueryFragmentRandomItemVideo = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.VideoFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// InnerQueryFragmentRandomLeafArticle includes the requested fields of the GraphQL type Article.
+type InnerQueryFragmentRandomLeafArticle struct {
+	Typename string `json:"__typename"`
+}
+
+// InnerQueryFragmentRandomLeafLeafContent includes the requested fields of the GraphQL interface LeafContent.
+//
+// InnerQueryFragmentRandomLeafLeafContent is implemented by the following types:
+// InnerQueryFragmentRandomLeafArticle
+// InnerQueryFragmentRandomLeafVideo
+//
+// The GraphQL type's documentation follows.
+//
+// LeafContent represents content items that can't have child-nodes.
+type InnerQueryFragmentRandomLeafLeafContent interface {
+	implementsGraphQLInterfaceInnerQueryFragmentRandomLeafLeafContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+}
+
+func (v *InnerQueryFragmentRandomLeafArticle) implementsGraphQLInterfaceInnerQueryFragmentRandomLeafLeafContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomLeafLeafContent.
+func (v *InnerQueryFragmentRandomLeafArticle) GetTypename() string { return v.Typename }
+
+func (v *InnerQueryFragmentRandomLeafVideo) implementsGraphQLInterfaceInnerQueryFragmentRandomLeafLeafContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomLeafLeafContent.
+func (v *InnerQueryFragmentRandomLeafVideo) GetTypename() string { return v.Typename }
+
+func __unmarshalInnerQueryFragmentRandomLeafLeafContent(v *InnerQueryFragmentRandomLeafLeafContent, 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(InnerQueryFragmentRandomLeafArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(InnerQueryFragmentRandomLeafVideo)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing LeafContent.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for InnerQueryFragmentRandomLeafLeafContent: "%v"`, tn.TypeName)
+	}
+}
+
+// InnerQueryFragmentRandomLeafVideo includes the requested fields of the GraphQL type Video.
+type InnerQueryFragmentRandomLeafVideo struct {
+	Typename        string `json:"__typename"`
+	VideoFields     `json:"-"`
+	MoreVideoFields `json:"-"`
+}
+
+func (v *InnerQueryFragmentRandomLeafVideo) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*InnerQueryFragmentRandomLeafVideo
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.InnerQueryFragmentRandomLeafVideo = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.VideoFields)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.MoreVideoFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// MoreVideoFields includes the GraphQL fields of Video requested by the fragment MoreVideoFields.
+type MoreVideoFields struct {
+	// ID is documented in the Content interface.
+	Id     *testutil.ID                `json:"id"`
+	Parent *MoreVideoFieldsParentTopic `json:"parent"`
+}
+
+// MoreVideoFieldsParentTopic includes the requested fields of the GraphQL type Topic.
+type MoreVideoFieldsParentTopic struct {
+	Name     *string                                     `json:"name"`
+	Url      *string                                     `json:"url"`
+	Children []MoreVideoFieldsParentTopicChildrenContent `json:"-"`
+}
+
+func (v *MoreVideoFieldsParentTopic) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*MoreVideoFieldsParentTopic
+		Children []json.RawMessage `json:"children"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.MoreVideoFieldsParentTopic = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.Children
+		raw := firstPass.Children
+		*target = make(
+			[]MoreVideoFieldsParentTopicChildrenContent,
+			len(raw))
+		for i, raw := range raw {
+			target := &(*target)[i]
+			err = __unmarshalMoreVideoFieldsParentTopicChildrenContent(
+				target, raw)
+			if err != nil {
+				return fmt.Errorf(
+					"Unable to unmarshal MoreVideoFieldsParentTopic.Children: %w", err)
+			}
+		}
+	}
+
+	return nil
+}
+
+// MoreVideoFieldsParentTopicChildrenArticle includes the requested fields of the GraphQL type Article.
+type MoreVideoFieldsParentTopicChildrenArticle struct {
+	Typename *string `json:"__typename"`
+}
+
+// MoreVideoFieldsParentTopicChildrenContent includes the requested fields of the GraphQL interface Content.
+//
+// MoreVideoFieldsParentTopicChildrenContent is implemented by the following types:
+// MoreVideoFieldsParentTopicChildrenArticle
+// MoreVideoFieldsParentTopicChildrenVideo
+// MoreVideoFieldsParentTopicChildrenTopic
+//
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type MoreVideoFieldsParentTopicChildrenContent interface {
+	implementsGraphQLInterfaceMoreVideoFieldsParentTopicChildrenContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() *string
+}
+
+func (v *MoreVideoFieldsParentTopicChildrenArticle) implementsGraphQLInterfaceMoreVideoFieldsParentTopicChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface MoreVideoFieldsParentTopicChildrenContent.
+func (v *MoreVideoFieldsParentTopicChildrenArticle) GetTypename() *string { return v.Typename }
+
+func (v *MoreVideoFieldsParentTopicChildrenVideo) implementsGraphQLInterfaceMoreVideoFieldsParentTopicChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface MoreVideoFieldsParentTopicChildrenContent.
+func (v *MoreVideoFieldsParentTopicChildrenVideo) GetTypename() *string { return v.Typename }
+
+func (v *MoreVideoFieldsParentTopicChildrenTopic) implementsGraphQLInterfaceMoreVideoFieldsParentTopicChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface MoreVideoFieldsParentTopicChildrenContent.
+func (v *MoreVideoFieldsParentTopicChildrenTopic) GetTypename() *string { return v.Typename }
+
+func __unmarshalMoreVideoFieldsParentTopicChildrenContent(v *MoreVideoFieldsParentTopicChildrenContent, 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(MoreVideoFieldsParentTopicChildrenArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(MoreVideoFieldsParentTopicChildrenVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(MoreVideoFieldsParentTopicChildrenTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for MoreVideoFieldsParentTopicChildrenContent: "%v"`, tn.TypeName)
+	}
+}
+
+// MoreVideoFieldsParentTopicChildrenTopic includes the requested fields of the GraphQL type Topic.
+type MoreVideoFieldsParentTopicChildrenTopic struct {
+	Typename *string `json:"__typename"`
+}
+
+// MoreVideoFieldsParentTopicChildrenVideo includes the requested fields of the GraphQL type Video.
+type MoreVideoFieldsParentTopicChildrenVideo struct {
+	Typename    *string `json:"__typename"`
+	VideoFields `json:"-"`
+}
+
+func (v *MoreVideoFieldsParentTopicChildrenVideo) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*MoreVideoFieldsParentTopicChildrenVideo
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.MoreVideoFieldsParentTopicChildrenVideo = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.VideoFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// QueryFragment includes the GraphQL fields of Query requested by the fragment QueryFragment.
+type QueryFragment struct {
+	InnerQueryFragment `json:"-"`
+}
+
+func (v *QueryFragment) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*QueryFragment
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.QueryFragment = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.InnerQueryFragment)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// VideoFields includes the GraphQL fields of Video requested by the fragment VideoFields.
+type VideoFields struct {
+	// ID is documented in the Content interface.
+	Id        testutil.ID          `json:"id"`
+	Name      string               `json:"name"`
+	Url       string               `json:"url"`
+	Duration  int                  `json:"duration"`
+	Thumbnail VideoFieldsThumbnail `json:"thumbnail"`
+}
+
+// VideoFieldsThumbnail includes the requested fields of the GraphQL type Thumbnail.
+type VideoFieldsThumbnail struct {
+	Id testutil.ID `json:"id"`
+}
+
+func ComplexNamedFragments(
+	client graphql.Client,
+) (*ComplexNamedFragmentsResponse, error) {
+	var err error
+
+	var retval ComplexNamedFragmentsResponse
+	err = client.MakeRequest(
+		nil,
+		"ComplexNamedFragments",
+		`
+query ComplexNamedFragments {
+	... on Query {
+		... QueryFragment
+	}
+}
+fragment QueryFragment on Query {
+	... InnerQueryFragment
+}
+fragment InnerQueryFragment on Query {
+	randomItem {
+		__typename
+		id
+		name
+		... VideoFields
+	}
+	randomLeaf {
+		__typename
+		... VideoFields
+		... MoreVideoFields
+	}
+	otherLeaf: randomLeaf {
+		__typename
+		... on Video {
+			... MoreVideoFields
+		}
+	}
+}
+fragment VideoFields on Video {
+	id
+	name
+	url
+	duration
+	thumbnail {
+		id
+	}
+}
+fragment MoreVideoFields on Video {
+	id
+	parent {
+		name
+		url
+		children {
+			__typename
+			... VideoFields
+		}
+	}
+}
+`,
+		&retval,
+		nil,
+	)
+	return &retval, err
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json
new file mode 100644
index 0000000..369d66a
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "ComplexNamedFragments",
+      "query": "\nquery ComplexNamedFragments {\n\t... on Query {\n\t\t... QueryFragment\n\t}\n}\nfragment QueryFragment on Query {\n\t... InnerQueryFragment\n}\nfragment InnerQueryFragment on Query {\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\tname\n\t\t... VideoFields\n\t}\n\trandomLeaf {\n\t\t__typename\n\t\t... VideoFields\n\t\t... MoreVideoFields\n\t}\n\totherLeaf: randomLeaf {\n\t\t__typename\n\t\t... on Video {\n\t\t\t... MoreVideoFields\n\t\t}\n\t}\n}\nfragment VideoFields on Video {\n\tid\n\tname\n\turl\n\tduration\n\tthumbnail {\n\t\tid\n\t}\n}\nfragment MoreVideoFields on Video {\n\tid\n\tparent {\n\t\tname\n\t\turl\n\t\tchildren {\n\t\t\t__typename\n\t\t\t... VideoFields\n\t\t}\n\t}\n}\n",
+      "sourceLocation": "testdata/queries/ComplexNamedFragments.graphql"
+    }
+  ]
+}
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go
index f42f68e..810b1e6 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go
@@ -26,13 +26,12 @@ type InterfaceListFieldRootTopic struct {
 
 func (v *InterfaceListFieldRootTopic) UnmarshalJSON(b []byte) error {
 
-	type InterfaceListFieldRootTopicWrapper InterfaceListFieldRootTopic
-
 	var firstPass struct {
-		*InterfaceListFieldRootTopicWrapper
+		*InterfaceListFieldRootTopic
 		Children []json.RawMessage `json:"children"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.InterfaceListFieldRootTopicWrapper = (*InterfaceListFieldRootTopicWrapper)(v)
+	firstPass.InterfaceListFieldRootTopic = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -55,6 +54,7 @@ func (v *InterfaceListFieldRootTopic) UnmarshalJSON(b []byte) error {
 			}
 		}
 	}
+
 	return nil
 }
 
@@ -183,13 +183,12 @@ type InterfaceListFieldWithPointerTopic struct {
 
 func (v *InterfaceListFieldWithPointerTopic) UnmarshalJSON(b []byte) error {
 
-	type InterfaceListFieldWithPointerTopicWrapper InterfaceListFieldWithPointerTopic
-
 	var firstPass struct {
-		*InterfaceListFieldWithPointerTopicWrapper
+		*InterfaceListFieldWithPointerTopic
 		Children []json.RawMessage `json:"children"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.InterfaceListFieldWithPointerTopicWrapper = (*InterfaceListFieldWithPointerTopicWrapper)(v)
+	firstPass.InterfaceListFieldWithPointerTopic = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -212,6 +211,7 @@ func (v *InterfaceListFieldWithPointerTopic) UnmarshalJSON(b []byte) error {
 			}
 		}
 	}
+
 	return nil
 }
 
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go
index db66082..1c8e4ef 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go
@@ -151,14 +151,13 @@ type InterfaceListOfListOfListsFieldResponse struct {
 
 func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error {
 
-	type InterfaceListOfListOfListsFieldResponseWrapper InterfaceListOfListOfListsFieldResponse
-
 	var firstPass struct {
-		*InterfaceListOfListOfListsFieldResponseWrapper
+		*InterfaceListOfListOfListsFieldResponse
 		ListOfListsOfListsOfContent [][][]json.RawMessage `json:"listOfListsOfListsOfContent"`
 		WithPointer                 [][][]json.RawMessage `json:"withPointer"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.InterfaceListOfListOfListsFieldResponseWrapper = (*InterfaceListOfListOfListsFieldResponseWrapper)(v)
+	firstPass.InterfaceListOfListOfListsFieldResponse = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -193,6 +192,7 @@ func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error
 			}
 		}
 	}
+
 	{
 		target := &v.WithPointer
 		raw := firstPass.WithPointer
@@ -222,6 +222,7 @@ func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error
 			}
 		}
 	}
+
 	return nil
 }
 
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go
index e38e2b0..82ecba7 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go
@@ -24,13 +24,12 @@ type InterfaceNestingRootTopic struct {
 
 func (v *InterfaceNestingRootTopic) UnmarshalJSON(b []byte) error {
 
-	type InterfaceNestingRootTopicWrapper InterfaceNestingRootTopic
-
 	var firstPass struct {
-		*InterfaceNestingRootTopicWrapper
+		*InterfaceNestingRootTopic
 		Children []json.RawMessage `json:"children"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.InterfaceNestingRootTopicWrapper = (*InterfaceNestingRootTopicWrapper)(v)
+	firstPass.InterfaceNestingRootTopic = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -53,6 +52,7 @@ func (v *InterfaceNestingRootTopic) UnmarshalJSON(b []byte) error {
 			}
 		}
 	}
+
 	return nil
 }
 
@@ -170,13 +170,12 @@ type InterfaceNestingRootTopicChildrenContentParentTopic struct {
 
 func (v *InterfaceNestingRootTopicChildrenContentParentTopic) UnmarshalJSON(b []byte) error {
 
-	type InterfaceNestingRootTopicChildrenContentParentTopicWrapper InterfaceNestingRootTopicChildrenContentParentTopic
-
 	var firstPass struct {
-		*InterfaceNestingRootTopicChildrenContentParentTopicWrapper
+		*InterfaceNestingRootTopicChildrenContentParentTopic
 		Children []json.RawMessage `json:"children"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.InterfaceNestingRootTopicChildrenContentParentTopicWrapper = (*InterfaceNestingRootTopicChildrenContentParentTopicWrapper)(v)
+	firstPass.InterfaceNestingRootTopicChildrenContentParentTopic = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -199,6 +198,7 @@ func (v *InterfaceNestingRootTopicChildrenContentParentTopic) UnmarshalJSON(b []
 			}
 		}
 	}
+
 	return nil
 }
 
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go
index aeff4ca..d23d766 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go
@@ -256,15 +256,14 @@ type InterfaceNoFragmentsQueryResponse struct {
 
 func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 
-	type InterfaceNoFragmentsQueryResponseWrapper InterfaceNoFragmentsQueryResponse
-
 	var firstPass struct {
-		*InterfaceNoFragmentsQueryResponseWrapper
+		*InterfaceNoFragmentsQueryResponse
 		RandomItem             json.RawMessage `json:"randomItem"`
 		RandomItemWithTypeName json.RawMessage `json:"randomItemWithTypeName"`
 		WithPointer            json.RawMessage `json:"withPointer"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.InterfaceNoFragmentsQueryResponseWrapper = (*InterfaceNoFragmentsQueryResponseWrapper)(v)
+	firstPass.InterfaceNoFragmentsQueryResponse = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -281,6 +280,7 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal InterfaceNoFragmentsQueryResponse.RandomItem: %w", err)
 		}
 	}
+
 	{
 		target := &v.RandomItemWithTypeName
 		raw := firstPass.RandomItemWithTypeName
@@ -291,6 +291,7 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal InterfaceNoFragmentsQueryResponse.RandomItemWithTypeName: %w", err)
 		}
 	}
+
 	{
 		target := &v.WithPointer
 		raw := firstPass.WithPointer
@@ -302,6 +303,7 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal InterfaceNoFragmentsQueryResponse.WithPointer: %w", err)
 		}
 	}
+
 	return nil
 }
 
diff --git a/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go
index 3afb18f..8fb67d3 100644
--- a/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go
@@ -134,13 +134,12 @@ type SimpleInlineFragmentResponse struct {
 
 func (v *SimpleInlineFragmentResponse) UnmarshalJSON(b []byte) error {
 
-	type SimpleInlineFragmentResponseWrapper SimpleInlineFragmentResponse
-
 	var firstPass struct {
-		*SimpleInlineFragmentResponseWrapper
+		*SimpleInlineFragmentResponse
 		RandomItem json.RawMessage `json:"randomItem"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.SimpleInlineFragmentResponseWrapper = (*SimpleInlineFragmentResponseWrapper)(v)
+	firstPass.SimpleInlineFragmentResponse = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -157,6 +156,7 @@ func (v *SimpleInlineFragmentResponse) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal SimpleInlineFragmentResponse.RandomItem: %w", err)
 		}
 	}
+
 	return nil
 }
 
diff --git a/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go
new file mode 100644
index 0000000..86078cf
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go
@@ -0,0 +1,334 @@
+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"
+)
+
+// SimpleNamedFragmentRandomItemArticle includes the requested fields of the GraphQL type Article.
+type SimpleNamedFragmentRandomItemArticle struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// SimpleNamedFragmentRandomItemContent includes the requested fields of the GraphQL interface Content.
+//
+// SimpleNamedFragmentRandomItemContent is implemented by the following types:
+// SimpleNamedFragmentRandomItemArticle
+// SimpleNamedFragmentRandomItemVideo
+// SimpleNamedFragmentRandomItemTopic
+//
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type SimpleNamedFragmentRandomItemContent interface {
+	implementsGraphQLInterfaceSimpleNamedFragmentRandomItemContent()
+	// 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 *SimpleNamedFragmentRandomItemArticle) implementsGraphQLInterfaceSimpleNamedFragmentRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent.
+func (v *SimpleNamedFragmentRandomItemArticle) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent.
+func (v *SimpleNamedFragmentRandomItemArticle) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent.
+func (v *SimpleNamedFragmentRandomItemArticle) GetName() string { return v.Name }
+
+func (v *SimpleNamedFragmentRandomItemVideo) implementsGraphQLInterfaceSimpleNamedFragmentRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent.
+func (v *SimpleNamedFragmentRandomItemVideo) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent.
+func (v *SimpleNamedFragmentRandomItemVideo) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent.
+func (v *SimpleNamedFragmentRandomItemVideo) GetName() string { return v.Name }
+
+func (v *SimpleNamedFragmentRandomItemTopic) implementsGraphQLInterfaceSimpleNamedFragmentRandomItemContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent.
+func (v *SimpleNamedFragmentRandomItemTopic) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent.
+func (v *SimpleNamedFragmentRandomItemTopic) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent.
+func (v *SimpleNamedFragmentRandomItemTopic) GetName() string { return v.Name }
+
+func __unmarshalSimpleNamedFragmentRandomItemContent(v *SimpleNamedFragmentRandomItemContent, 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(SimpleNamedFragmentRandomItemArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(SimpleNamedFragmentRandomItemVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(SimpleNamedFragmentRandomItemTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for SimpleNamedFragmentRandomItemContent: "%v"`, tn.TypeName)
+	}
+}
+
+// SimpleNamedFragmentRandomItemTopic includes the requested fields of the GraphQL type Topic.
+type SimpleNamedFragmentRandomItemTopic struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// SimpleNamedFragmentRandomItemVideo includes the requested fields of the GraphQL type Video.
+type SimpleNamedFragmentRandomItemVideo struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id          testutil.ID `json:"id"`
+	Name        string      `json:"name"`
+	VideoFields `json:"-"`
+}
+
+func (v *SimpleNamedFragmentRandomItemVideo) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*SimpleNamedFragmentRandomItemVideo
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.SimpleNamedFragmentRandomItemVideo = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.VideoFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// SimpleNamedFragmentRandomLeafArticle includes the requested fields of the GraphQL type Article.
+type SimpleNamedFragmentRandomLeafArticle struct {
+	Typename string `json:"__typename"`
+}
+
+// SimpleNamedFragmentRandomLeafLeafContent includes the requested fields of the GraphQL interface LeafContent.
+//
+// SimpleNamedFragmentRandomLeafLeafContent is implemented by the following types:
+// SimpleNamedFragmentRandomLeafArticle
+// SimpleNamedFragmentRandomLeafVideo
+//
+// The GraphQL type's documentation follows.
+//
+// LeafContent represents content items that can't have child-nodes.
+type SimpleNamedFragmentRandomLeafLeafContent interface {
+	implementsGraphQLInterfaceSimpleNamedFragmentRandomLeafLeafContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+}
+
+func (v *SimpleNamedFragmentRandomLeafArticle) implementsGraphQLInterfaceSimpleNamedFragmentRandomLeafLeafContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomLeafLeafContent.
+func (v *SimpleNamedFragmentRandomLeafArticle) GetTypename() string { return v.Typename }
+
+func (v *SimpleNamedFragmentRandomLeafVideo) implementsGraphQLInterfaceSimpleNamedFragmentRandomLeafLeafContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomLeafLeafContent.
+func (v *SimpleNamedFragmentRandomLeafVideo) GetTypename() string { return v.Typename }
+
+func __unmarshalSimpleNamedFragmentRandomLeafLeafContent(v *SimpleNamedFragmentRandomLeafLeafContent, 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(SimpleNamedFragmentRandomLeafArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(SimpleNamedFragmentRandomLeafVideo)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing LeafContent.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for SimpleNamedFragmentRandomLeafLeafContent: "%v"`, tn.TypeName)
+	}
+}
+
+// SimpleNamedFragmentRandomLeafVideo includes the requested fields of the GraphQL type Video.
+type SimpleNamedFragmentRandomLeafVideo struct {
+	Typename    string `json:"__typename"`
+	VideoFields `json:"-"`
+}
+
+func (v *SimpleNamedFragmentRandomLeafVideo) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*SimpleNamedFragmentRandomLeafVideo
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.SimpleNamedFragmentRandomLeafVideo = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(b, &v.VideoFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// SimpleNamedFragmentResponse is returned by SimpleNamedFragment on success.
+type SimpleNamedFragmentResponse struct {
+	RandomItem SimpleNamedFragmentRandomItemContent     `json:"-"`
+	RandomLeaf SimpleNamedFragmentRandomLeafLeafContent `json:"-"`
+}
+
+func (v *SimpleNamedFragmentResponse) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*SimpleNamedFragmentResponse
+		RandomItem json.RawMessage `json:"randomItem"`
+		RandomLeaf json.RawMessage `json:"randomLeaf"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.SimpleNamedFragmentResponse = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.RandomItem
+		raw := firstPass.RandomItem
+		err = __unmarshalSimpleNamedFragmentRandomItemContent(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal SimpleNamedFragmentResponse.RandomItem: %w", err)
+		}
+	}
+
+	{
+		target := &v.RandomLeaf
+		raw := firstPass.RandomLeaf
+		err = __unmarshalSimpleNamedFragmentRandomLeafLeafContent(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal SimpleNamedFragmentResponse.RandomLeaf: %w", err)
+		}
+	}
+
+	return nil
+}
+
+// VideoFields includes the GraphQL fields of Video requested by the fragment VideoFields.
+type VideoFields struct {
+	// ID is documented in the Content interface.
+	Id        testutil.ID          `json:"id"`
+	Name      string               `json:"name"`
+	Url       string               `json:"url"`
+	Duration  int                  `json:"duration"`
+	Thumbnail VideoFieldsThumbnail `json:"thumbnail"`
+}
+
+// VideoFieldsThumbnail includes the requested fields of the GraphQL type Thumbnail.
+type VideoFieldsThumbnail struct {
+	Id testutil.ID `json:"id"`
+}
+
+func SimpleNamedFragment(
+	client graphql.Client,
+) (*SimpleNamedFragmentResponse, error) {
+	var err error
+
+	var retval SimpleNamedFragmentResponse
+	err = client.MakeRequest(
+		nil,
+		"SimpleNamedFragment",
+		`
+query SimpleNamedFragment {
+	randomItem {
+		__typename
+		id
+		name
+		... VideoFields
+	}
+	randomLeaf {
+		__typename
+		... VideoFields
+	}
+}
+fragment VideoFields on Video {
+	id
+	name
+	url
+	duration
+	thumbnail {
+		id
+	}
+}
+`,
+		&retval,
+		nil,
+	)
+	return &retval, err
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.json b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.json
new file mode 100644
index 0000000..edfc888
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "SimpleNamedFragment",
+      "query": "\nquery SimpleNamedFragment {\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\tname\n\t\t... VideoFields\n\t}\n\trandomLeaf {\n\t\t__typename\n\t\t... VideoFields\n\t}\n}\nfragment VideoFields on Video {\n\tid\n\tname\n\turl\n\tduration\n\tthumbnail {\n\t\tid\n\t}\n}\n",
+      "sourceLocation": "testdata/queries/SimpleNamedFragment.graphql"
+    }
+  ]
+}
diff --git a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go
index a31e28c..14f7e4b 100644
--- a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go
@@ -82,13 +82,12 @@ type UnionNoFragmentsQueryResponse struct {
 
 func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 
-	type UnionNoFragmentsQueryResponseWrapper UnionNoFragmentsQueryResponse
-
 	var firstPass struct {
-		*UnionNoFragmentsQueryResponseWrapper
+		*UnionNoFragmentsQueryResponse
 		RandomLeaf json.RawMessage `json:"randomLeaf"`
+		graphql.NoUnmarshalJSON
 	}
-	firstPass.UnionNoFragmentsQueryResponseWrapper = (*UnionNoFragmentsQueryResponseWrapper)(v)
+	firstPass.UnionNoFragmentsQueryResponse = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
@@ -105,6 +104,7 @@ func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 				"Unable to unmarshal UnionNoFragmentsQueryResponse.RandomLeaf: %w", err)
 		}
 	}
+
 	return nil
 }
 
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index a201c12..0cfd723 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -288,6 +288,94 @@ func TestFragments(t *testing.T) {
 	assert.Nil(t, resp.Beings[2])
 }
 
+func TestNamedFragments(t *testing.T) {
+	_ = `# @genqlient
+	fragment AnimalFields on Animal {
+		id
+		hair { hasHair }
+		owner { id ...UserFields }
+	}
+
+	fragment MoreUserFields on User {
+		id
+		hair { color }
+	}
+	
+	fragment UserFields on User {
+		id luckyNumber
+		...MoreUserFields
+	}
+
+	query queryWithNamedFragments($ids: [ID!]!) {
+		beings(ids: $ids) {
+			__typename id
+			...AnimalFields
+			...UserFields
+		}
+	}`
+
+	ctx := context.Background()
+	server := server.RunServer()
+	defer server.Close()
+	client := graphql.NewClient(server.URL, http.DefaultClient)
+
+	resp, err := queryWithNamedFragments(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 luckyNumber.
+	assert.Equal(t, "User", resp.Beings[0].GetTypename())
+	assert.Equal(t, "1", resp.Beings[0].GetId())
+	// (luckyNumber, hair we need to cast for)
+
+	user, ok := resp.Beings[0].(*queryWithNamedFragmentsBeingsUser)
+	require.Truef(t, ok, "got %T, not User", resp.Beings[0])
+	assert.Equal(t, "1", user.Id)
+	assert.Equal(t, "1", user.UserFields.Id)
+	assert.Equal(t, "1", user.UserFields.MoreUserFields.Id)
+	// on UserFields, but we should be able to access directly via embedding:
+	assert.Equal(t, 17, user.LuckyNumber)
+	assert.Equal(t, "Black", user.Hair.Color)
+
+	// Animal has, in total, the fields:
+	//	__typename
+	//	id
+	//	hair { hasHair }
+	//	owner { id luckyNumber }
+	assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
+	assert.Equal(t, "3", resp.Beings[1].GetId())
+	// (hair.* and owner.* we have to cast for)
+
+	animal, ok := resp.Beings[1].(*queryWithNamedFragmentsBeingsAnimal)
+	require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
+	// Check that we filled in *both* ID fields:
+	assert.Equal(t, "3", animal.Id)
+	assert.Equal(t, "3", animal.AnimalFields.Id)
+	// on AnimalFields:
+	assert.True(t, animal.Hair.HasHair)
+	assert.Equal(t, "1", animal.Owner.GetId())
+	// (luckyNumber we have to cast for, again)
+
+	owner, ok := animal.Owner.(*AnimalFieldsOwnerUser)
+	require.Truef(t, ok, "got %T, not User", animal.Owner)
+	// Check that we filled in *both* ID fields:
+	assert.Equal(t, "1", owner.Id)
+	assert.Equal(t, "1", owner.UserFields.Id)
+	assert.Equal(t, "1", owner.UserFields.MoreUserFields.Id)
+	// on UserFields:
+	assert.Equal(t, 17, owner.LuckyNumber)
+	assert.Equal(t, "Black", owner.Hair.Color)
+
+	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