Repository: Khan/genqlient
Pull request #121 (2021-09-29 23:55:15)

Add support for "flattening" fragment-spreads
## Summary:
One common use of fragment spreads is as the entirety of a field's
selection, e.g.
```graphql
query MyQuery {
  myField {
    ...MyFragment
  }
}
```
In this case, by default, genqlient generates a wrapper type
`MyQueryMyFieldMyType`, which just embeds `MyFragment`.  This makes
sense if you later want to add more fields in addition to the fragment
spread.  But if you don't -- and you did the fragment because you want
to share types, it's an extra layer of indirection.  (Which becomes
especially onerous if `myField` has list type (`[MyType!]`), such that
it's not just an extra attribute-access to get to `MyFragment`.)

The new option `# @genqlient(flatten: true)` simplifies this situation:
if applied to `myField` is skips the wrapper type;
`MyQueryResponse.MyField` will simply have type `MyFragment` (or
`[]MyFragment`, or whatever).  This should hopefully make the `typename`
option, which has more limitations, less necessary.

Note that in #30 the initial idea was to support this for fields as
well.  This would require significant additional complexity in the
JSON-(un)marshaling code, and has proven less necessary, so I
implemented this option only for fragment-spreads for now.  With that
restriction, it was shockingly simple; we have to hook into a bunch of
different places, but they're all quite simple, since the structure of
the Go types still matches the structure in GraphQL.

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

## Test plan:
make check


benjaminjkraft: > The logic is straightforward enough, as you point out; what I don't have a good handle on is if there's another place you need to add the flatten-check besides the 4 (!) you have now. I kinda-understand the first three of them: a containing query, struct, or interface/union. But I don't really understand the last one: if we're already doing the check on the container, why do we need to do the check on the fragment-spread itself? Is this for cases where we have nested fragment-spreads or something?

The check is on the fragment-definition -- which is indeed for nested fragment-spreads, namely where you do e.g.
```graphql
# @genqlient(flatten: true)
fragment Outer on T {
  ...Inner
}
```
in which case `Outer` effectively becomes an alias for `Inner`.  (Actually we don't even bother to define it, although doing all the flattening as type aliases would be an interesting idea.)  I'm not sure why you'd do that, but I figured there's no reason not to allow it.



Patch:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index a845124..725e3b7 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -25,6 +25,7 @@ When releasing a new version:
 ### New features:
 
 - genqlient's types are now safe to JSON-marshal, which can be useful for putting them in a cache, for example.  See the [docs](FAQ.md#-let-me-json-marshal-my-response-objects) for details.
+- The new `flatten` option in the `# @genqlient` directive allows for a simpler form of type-sharing using fragment spreads.  See the [docs](FAQ.md#-shared-types-between-different-parts-of-the-query) for details.
 
 ### Bug fixes:
 
diff --git a/docs/FAQ.md b/docs/FAQ.md
index 7d24a0a..9bd58f1 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -260,9 +260,9 @@ type GetMonopolyPlayersGameWinnerUser struct {
 // (others similarly)
 ```
 
-But maybe you wanted to be able to pass all those users to a shared function (defined in your code), say `FormatUser(user ???) string`.  That's no good; you need to put three different types as the `???`.  genqlient has two ways to deal with this.
+But maybe you wanted to be able to pass all those users to a shared function (defined in your code), say `FormatUser(user ???) string`.  That's no good; you need to put three different types as the `???`.  genqlient has several ways to deal with this.
 
-One option -- the GraphQL Way, perhaps -- is to use fragments.  You'd write your query like:
+**Fragments:** One option -- the GraphQL Way, perhaps -- is to use fragments.  You'd write your query like:
 
 ```graphql
 fragment MonopolyUser on User {
@@ -319,7 +319,21 @@ query GetMonopolyPlayers {
 
 and you can even spread the fragment into interface types.  It also avoids having to list the fields several times.
 
-Alternately, if you always want exactly the same fields, you can use the simpler but more restrictive genqlient option `typename`:
+**Fragments, flattened:** The Go field for `winner`, in the first query above, has type `GetMonopolyPlayersGameWinnerUser` which just wraps `MonopolyUser`.  If we don't want to add any other fields, that's unnecessary!  Instead, we could do
+```
+query GetMonopolyPlayers {
+  game {
+    # @genqlient(flatten: true)
+    winner {
+      ...MonopolyUser
+    }
+    # (etc.)
+  }
+}
+```
+and genqlient will skip the indirection and give the field `Winner` type `MonopolyUser` directly.  This is often much more convenient if you put all the fields in the fragment, like the first query did.  See the [options documentation](genqlient_directive.graphql) for more details.
+
+**Type names:** Finally, if you always want exactly the same fields, you can use the simpler but more restrictive genqlient option `typename`:
 
 ```graphql
 query GetMonopolyPlayers {
@@ -351,7 +365,7 @@ type User struct {
 
 In this case, genqlient will validate that each type given the name `User` has the exact same fields; see the [full documentation](genqlient_directive.graphql) for details.
 
-Note that it's also possible to use the `bindings` option (see [`genqlient.yaml` documentation](genqlient.yaml)) for a similar purpose, but this is not recommended as it typically requires more work for less gain.
+**Bindings:** It's also possible to use the `bindings` option (see [`genqlient.yaml` documentation](genqlient.yaml)) for a similar purpose, but this is not recommended as it typically requires more work for less gain.
 
 ### … documentation on the output types?
 
diff --git a/docs/genqlient_directive.graphql b/docs/genqlient_directive.graphql
index d00a8fe..b3d7f18 100644
--- a/docs/genqlient_directive.graphql
+++ b/docs/genqlient_directive.graphql
@@ -82,6 +82,40 @@ directive genqlient(
   # fragment, you'll have to remove this option, and the types will change.
   struct: Boolean
 
+  # If set, this field's selection must contain a single fragment-spread; we'll
+  # use the type of that fragment-spread as the type of the field.
+  #
+  # For example, given a query like
+  #  query MyQuery {
+  #    myField {
+  #      ...MyFragment
+  #    }
+  #  }
+  # by default genqlient will generate these types:
+  #  type MyQueryResponse struct {
+  #    MyField MyQueryMyFieldMyType
+  #  }
+  #  type MyQueryMyFieldMyType struct {
+  #    MyFragment
+  #  }
+  # If we instead do:
+  #  query MyQuery {
+  #    # @genqlient(flatten: true)
+  #    myField {
+  #      ...MyFragment
+  #    }
+  #  }
+  # genqlient will simplify things:
+  #  type MyQueryResponse struct {
+  #    MyField MyFragment
+  #  }
+  #
+  # This is only applicable to fields whose selection is a single
+  # fragment-spread, such that the field-type implements the fragment-type
+  # (i.e. we can't do this if MyFragment is on one implementation of the type
+  # of MyField; what if we got back the other type?).
+  flatten: Boolean
+
   # If set, this argument or field will use the given Go type instead of a
   # genqlient-generated type.
   #
@@ -128,7 +162,7 @@ directive genqlient(
   # if your type-name conflicts with an autogenerated one (again, unless they
   # request the exact same fields).  They must even have the fields in the
   # same order.  Fragments are often easier to use (see the discussion of
-  # code-sharing in FAQ.md).
+  # code-sharing in FAQ.md, and the "flatten" option above).
   #
   # Note that unlike most directives, if applied to the entire operation,
   # typename affects the overall response type, rather than being propagated
diff --git a/generate/convert.go b/generate/convert.go
index 889d18e..8f9ceb8 100644
--- a/generate/convert.go
+++ b/generate/convert.go
@@ -115,6 +115,15 @@ func (g *generator) convertOperation(
 		return nil, err
 	}
 
+	// It's not common to use a fragment-spread for the whole query, but you
+	// can if you want two queries to return the same type!
+	if queryOptions.GetFlatten() {
+		i, err := validateFlattenOption(baseType, operation.SelectionSet, operation.Position)
+		if err == nil {
+			return fields[i].GoType, nil
+		}
+	}
+
 	goType := &goStructType{
 		GoName: name,
 		descriptionInfo: descriptionInfo{
@@ -340,6 +349,17 @@ func (g *generator) convertDefinition(
 		if err != nil {
 			return nil, err
 		}
+		if options.GetFlatten() {
+			// As with struct, flatten only applies if valid, important if you
+			// applied it to the whole query.
+			// TODO(benkraft): This is a slightly fragile way to do this;
+			// figure out a good way to do it before/while constructing the
+			// fields, rather than after.
+			i, err := validateFlattenOption(def, selectionSet, pos)
+			if err == nil {
+				return fields[i].GoType, nil
+			}
+		}
 
 		goType := &goStructType{
 			GoName:          name,
@@ -406,6 +426,14 @@ func (g *generator) convertDefinition(
 		if err != nil {
 			return nil, err
 		}
+		// Flatten can only flatten if there is only one field (plus perhaps
+		// __typename), and it's shared.
+		if options.GetFlatten() {
+			i, err := validateFlattenOption(def, selectionSet, pos)
+			if err == nil {
+				return sharedFields[i].GoType, nil
+			}
+		}
 
 		implementationTypes := g.schema.GetPossibleTypes(def)
 		goType := &goInterfaceType{
@@ -705,6 +733,15 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy
 	if err != nil {
 		return nil, err
 	}
+	if directive.GetFlatten() {
+		// Flatten on a fragment-definition is a bit weird -- it makes one
+		// fragment effectively an alias for another -- but no reason we can't
+		// allow it.
+		i, err := validateFlattenOption(typ, fragment.SelectionSet, fragment.Position)
+		if err == nil {
+			return fields[i].GoType, nil
+		}
+	}
 
 	switch typ.Kind {
 	case ast.Object:
diff --git a/generate/genqlient_directive.go b/generate/genqlient_directive.go
index 15ed1c8..903c83f 100644
--- a/generate/genqlient_directive.go
+++ b/generate/genqlient_directive.go
@@ -15,6 +15,7 @@ type genqlientDirective struct {
 	Omitempty *bool
 	Pointer   *bool
 	Struct    *bool
+	Flatten   *bool
 	Bind      string
 	TypeName  string
 }
@@ -28,6 +29,7 @@ func newGenqlientDirective(pos *ast.Position) *genqlientDirective {
 func (dir *genqlientDirective) GetOmitempty() bool { return dir.Omitempty != nil && *dir.Omitempty }
 func (dir *genqlientDirective) GetPointer() bool   { return dir.Pointer != nil && *dir.Pointer }
 func (dir *genqlientDirective) GetStruct() bool    { return dir.Struct != nil && *dir.Struct }
+func (dir *genqlientDirective) GetFlatten() bool   { return dir.Flatten != nil && *dir.Flatten }
 
 func setBool(optionName string, dst **bool, v *ast.Value, pos *ast.Position) error {
 	if *dst != nil {
@@ -85,6 +87,8 @@ func (dir *genqlientDirective) add(graphQLDirective *ast.Directive, pos *ast.Pos
 			err = setBool("pointer", &dir.Pointer, arg.Value, pos)
 		case "struct":
 			err = setBool("struct", &dir.Struct, arg.Value, pos)
+		case "flatten":
+			err = setBool("flatten", &dir.Flatten, arg.Value, pos)
 		case "bind":
 			err = setString("bind", &dir.Bind, arg.Value, pos)
 		case "typename":
@@ -116,7 +120,7 @@ func (dir *genqlientDirective) validate(node interface{}, schema *ast.Schema) er
 		}
 
 		if dir.Struct != nil {
-			return errorf(dir.pos, "struct is only applicable to fields")
+			return errorf(dir.pos, "struct is only applicable to fields, not frragment-definitions")
 		}
 
 		// Like operations, anything else will just apply to the entire
@@ -128,22 +132,32 @@ func (dir *genqlientDirective) validate(node interface{}, schema *ast.Schema) er
 		}
 
 		if dir.Struct != nil {
-			return errorf(dir.pos, "struct is only applicable to fields")
+			return errorf(dir.pos, "struct is only applicable to fields, not variable-definitions")
+		}
+
+		if dir.Flatten != nil {
+			return errorf(dir.pos, "flatten is only applicable to fields, not variable-definitions")
 		}
 
 		return nil
 	case *ast.Field:
 		if dir.Omitempty != nil {
-			return errorf(dir.pos, "omitempty is not applicable to fields")
+			return errorf(dir.pos, "omitempty is not applicable to variables, not fields")
 		}
 
+		typ := schema.Types[node.Definition.Type.Name()]
 		if dir.Struct != nil {
-			typ := schema.Types[node.Definition.Type.Name()]
 			if err := validateStructOption(typ, node.SelectionSet, dir.pos); err != nil {
 				return err
 			}
 		}
 
+		if dir.Flatten != nil {
+			if _, err := validateFlattenOption(typ, node.SelectionSet, dir.pos); err != nil {
+				return err
+			}
+		}
+
 		return nil
 	default:
 		return errorf(dir.pos, "invalid @genqlient directive location: %T", node)
@@ -178,6 +192,58 @@ func validateStructOption(
 	return nil
 }
 
+func validateFlattenOption(
+	typ *ast.Definition,
+	selectionSet ast.SelectionSet,
+	pos *ast.Position,
+) (index int, err error) {
+	index = -1
+	if len(selectionSet) == 0 {
+		return -1, errorf(pos, "flatten is not allowed for leaf fields")
+	}
+
+	for i, selection := range selectionSet {
+		switch selection := selection.(type) {
+		case *ast.Field:
+			// If the field is auto-added __typename, ignore it for flattening
+			// purposes.
+			if selection.Name == "__typename" && selection.Position == nil {
+				continue
+			}
+			// Type-wise, it's no harder to implement flatten for fields, but
+			// it requires new logic in UnmarshalJSON.  We can add that if it
+			// proves useful relative to its complexity.
+			return -1, errorf(pos, "flatten is not yet supported for fields (only fragment spreads)")
+
+		case *ast.InlineFragment:
+			// Inline fragments aren't allowed. In principle there's nothing
+			// stopping us from allowing them (under the same type-match
+			// conditions as fragment spreads), but there's little value to it.
+			return -1, errorf(pos, "flatten is not allowed for selections with inline fragments")
+
+		case *ast.FragmentSpread:
+			if index != -1 {
+				return -1, errorf(pos, "flatten is not allowed for fields with multiple selections")
+			} else if !fragmentMatches(typ, selection.Definition.Definition) {
+				// We don't let you flatten
+				//  field { # type: FieldType
+				//		...Fragment # type: FragmentType
+				//	}
+				// unless FragmentType implements FieldType, because otherwise
+				// what do we do if we get back a type that doesn't implement
+				// FragmentType?
+				return -1, errorf(pos,
+					"flatten is not allowed for fields with fragment-spreads "+
+						"unless the field-type implements the fragment-type; "+
+						"field-type %s does not implement fragment-type %s",
+					typ.Name, selection.Definition.Definition.Name)
+			}
+			index = i
+		}
+	}
+	return index, nil
+}
+
 // merge joins the directive applied to this node (the argument) and the one
 // applied to the entire operation (the receiver) and returns a new
 // directive-object representing the options to apply to this node (where in
@@ -193,6 +259,9 @@ func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirect
 	if other.Struct != nil {
 		retval.Struct = other.Struct
 	}
+	if other.Flatten != nil {
+		retval.Flatten = other.Flatten
+	}
 	if other.Bind != "" {
 		retval.Bind = other.Bind
 	}
diff --git a/internal/integration/generated.go b/internal/integration/generated.go
index f2f4dc6..f6674c3 100644
--- a/internal/integration/generated.go
+++ b/internal/integration/generated.go
@@ -255,6 +255,177 @@ func (v *AnimalFieldsOwnerUser) __premarshalJSON() (*__premarshalAnimalFieldsOwn
 	return &retval, nil
 }
 
+// FriendsFields includes the GraphQL fields of User requested by the fragment FriendsFields.
+type FriendsFields struct {
+	Id   string `json:"id"`
+	Name string `json:"name"`
+}
+
+// InnerBeingFields includes the GraphQL fields of Being requested by the fragment InnerBeingFields.
+//
+// InnerBeingFields is implemented by the following types:
+// InnerBeingFieldsUser
+// InnerBeingFieldsAnimal
+type InnerBeingFields interface {
+	implementsGraphQLInterfaceInnerBeingFields()
+	// GetId returns the interface-field "id" from its implementation.
+	GetId() string
+	// GetName returns the interface-field "name" from its implementation.
+	GetName() string
+}
+
+func (v *InnerBeingFieldsUser) implementsGraphQLInterfaceInnerBeingFields() {}
+
+// GetId is a part of, and documented with, the interface InnerBeingFields.
+func (v *InnerBeingFieldsUser) GetId() string { return v.Id }
+
+// GetName is a part of, and documented with, the interface InnerBeingFields.
+func (v *InnerBeingFieldsUser) GetName() string { return v.Name }
+
+func (v *InnerBeingFieldsAnimal) implementsGraphQLInterfaceInnerBeingFields() {}
+
+// GetId is a part of, and documented with, the interface InnerBeingFields.
+func (v *InnerBeingFieldsAnimal) GetId() string { return v.Id }
+
+// GetName is a part of, and documented with, the interface InnerBeingFields.
+func (v *InnerBeingFieldsAnimal) GetName() string { return v.Name }
+
+func __unmarshalInnerBeingFields(b []byte, v *InnerBeingFields) error {
+	if string(b) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(b, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "User":
+		*v = new(InnerBeingFieldsUser)
+		return json.Unmarshal(b, *v)
+	case "Animal":
+		*v = new(InnerBeingFieldsAnimal)
+		return json.Unmarshal(b, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Being.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for InnerBeingFields: "%v"`, tn.TypeName)
+	}
+}
+
+func __marshalInnerBeingFields(v *InnerBeingFields) ([]byte, error) {
+
+	var typename string
+	switch v := (*v).(type) {
+	case *InnerBeingFieldsUser:
+		typename = "User"
+
+		result := struct {
+			TypeName string `json:"__typename"`
+			*InnerBeingFieldsUser
+		}{typename, v}
+		return json.Marshal(result)
+	case *InnerBeingFieldsAnimal:
+		typename = "Animal"
+
+		result := struct {
+			TypeName string `json:"__typename"`
+			*InnerBeingFieldsAnimal
+		}{typename, v}
+		return json.Marshal(result)
+	case nil:
+		return []byte("null"), nil
+	default:
+		return nil, fmt.Errorf(
+			`Unexpected concrete type for InnerBeingFields: "%T"`, v)
+	}
+}
+
+// InnerBeingFields includes the GraphQL fields of Animal requested by the fragment InnerBeingFields.
+type InnerBeingFieldsAnimal struct {
+	Id   string `json:"id"`
+	Name string `json:"name"`
+}
+
+// InnerBeingFields includes the GraphQL fields of User requested by the fragment InnerBeingFields.
+type InnerBeingFieldsUser struct {
+	Id      string          `json:"id"`
+	Name    string          `json:"name"`
+	Friends []FriendsFields `json:"friends"`
+}
+
+// InnerLuckyFields includes the GraphQL fields of Lucky requested by the fragment InnerLuckyFields.
+//
+// InnerLuckyFields is implemented by the following types:
+// InnerLuckyFieldsUser
+type InnerLuckyFields interface {
+	implementsGraphQLInterfaceInnerLuckyFields()
+	// GetLuckyNumber returns the interface-field "luckyNumber" from its implementation.
+	GetLuckyNumber() int
+}
+
+func (v *InnerLuckyFieldsUser) implementsGraphQLInterfaceInnerLuckyFields() {}
+
+// GetLuckyNumber is a part of, and documented with, the interface InnerLuckyFields.
+func (v *InnerLuckyFieldsUser) GetLuckyNumber() int { return v.LuckyNumber }
+
+func __unmarshalInnerLuckyFields(b []byte, v *InnerLuckyFields) error {
+	if string(b) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(b, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "User":
+		*v = new(InnerLuckyFieldsUser)
+		return json.Unmarshal(b, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Lucky.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for InnerLuckyFields: "%v"`, tn.TypeName)
+	}
+}
+
+func __marshalInnerLuckyFields(v *InnerLuckyFields) ([]byte, error) {
+
+	var typename string
+	switch v := (*v).(type) {
+	case *InnerLuckyFieldsUser:
+		typename = "User"
+
+		result := struct {
+			TypeName string `json:"__typename"`
+			*InnerLuckyFieldsUser
+		}{typename, v}
+		return json.Marshal(result)
+	case nil:
+		return []byte("null"), nil
+	default:
+		return nil, fmt.Errorf(
+			`Unexpected concrete type for InnerLuckyFields: "%T"`, v)
+	}
+}
+
+// InnerLuckyFields includes the GraphQL fields of User requested by the fragment InnerLuckyFields.
+type InnerLuckyFieldsUser struct {
+	LuckyNumber int `json:"luckyNumber"`
+}
+
 // LuckyFields includes the GraphQL fields of Lucky requested by the fragment LuckyFields.
 //
 // LuckyFields is implemented by the following types:
@@ -387,6 +558,313 @@ type MoreUserFieldsHair struct {
 	Color string `json:"color"`
 }
 
+// QueryFragment includes the GraphQL fields of Query requested by the fragment QueryFragment.
+type QueryFragment struct {
+	Beings []QueryFragmentBeingsBeing `json:"-"`
+}
+
+func (v *QueryFragment) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*QueryFragment
+		Beings []json.RawMessage `json:"beings"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.QueryFragment = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		dst := &v.Beings
+		src := firstPass.Beings
+		*dst = make(
+			[]QueryFragmentBeingsBeing,
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
+			if len(src) != 0 && string(src) != "null" {
+				err = __unmarshalQueryFragmentBeingsBeing(
+					src, dst)
+				if err != nil {
+					return fmt.Errorf(
+						"Unable to unmarshal QueryFragment.Beings: %w", err)
+				}
+			}
+		}
+	}
+	return nil
+}
+
+type __premarshalQueryFragment struct {
+	Beings []json.RawMessage `json:"beings"`
+}
+
+func (v *QueryFragment) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *QueryFragment) __premarshalJSON() (*__premarshalQueryFragment, error) {
+	var retval __premarshalQueryFragment
+
+	{
+
+		dst := &retval.Beings
+		src := v.Beings
+		*dst = make(
+			[]json.RawMessage,
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
+			var err error
+			*dst, err = __marshalQueryFragmentBeingsBeing(
+				&src)
+			if err != nil {
+				return nil, fmt.Errorf(
+					"Unable to marshal QueryFragment.Beings: %w", err)
+			}
+		}
+	}
+	return &retval, nil
+}
+
+// QueryFragmentBeingsAnimal includes the requested fields of the GraphQL type Animal.
+type QueryFragmentBeingsAnimal struct {
+	Typename string           `json:"__typename"`
+	Id       string           `json:"id"`
+	Owner    InnerBeingFields `json:"-"`
+}
+
+func (v *QueryFragmentBeingsAnimal) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*QueryFragmentBeingsAnimal
+		Owner json.RawMessage `json:"owner"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.QueryFragmentBeingsAnimal = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		dst := &v.Owner
+		src := firstPass.Owner
+		if len(src) != 0 && string(src) != "null" {
+			err = __unmarshalInnerBeingFields(
+				src, dst)
+			if err != nil {
+				return fmt.Errorf(
+					"Unable to unmarshal QueryFragmentBeingsAnimal.Owner: %w", err)
+			}
+		}
+	}
+	return nil
+}
+
+type __premarshalQueryFragmentBeingsAnimal struct {
+	Typename string `json:"__typename"`
+
+	Id string `json:"id"`
+
+	Owner json.RawMessage `json:"owner"`
+}
+
+func (v *QueryFragmentBeingsAnimal) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *QueryFragmentBeingsAnimal) __premarshalJSON() (*__premarshalQueryFragmentBeingsAnimal, error) {
+	var retval __premarshalQueryFragmentBeingsAnimal
+
+	retval.Typename = v.Typename
+	retval.Id = v.Id
+	{
+
+		dst := &retval.Owner
+		src := v.Owner
+		var err error
+		*dst, err = __marshalInnerBeingFields(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"Unable to marshal QueryFragmentBeingsAnimal.Owner: %w", err)
+		}
+	}
+	return &retval, nil
+}
+
+// QueryFragmentBeingsBeing includes the requested fields of the GraphQL interface Being.
+//
+// QueryFragmentBeingsBeing is implemented by the following types:
+// QueryFragmentBeingsUser
+// QueryFragmentBeingsAnimal
+type QueryFragmentBeingsBeing interface {
+	implementsGraphQLInterfaceQueryFragmentBeingsBeing()
+	// 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 *QueryFragmentBeingsUser) implementsGraphQLInterfaceQueryFragmentBeingsBeing() {}
+
+// GetTypename is a part of, and documented with, the interface QueryFragmentBeingsBeing.
+func (v *QueryFragmentBeingsUser) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface QueryFragmentBeingsBeing.
+func (v *QueryFragmentBeingsUser) GetId() string { return v.Id }
+
+func (v *QueryFragmentBeingsAnimal) implementsGraphQLInterfaceQueryFragmentBeingsBeing() {}
+
+// GetTypename is a part of, and documented with, the interface QueryFragmentBeingsBeing.
+func (v *QueryFragmentBeingsAnimal) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface QueryFragmentBeingsBeing.
+func (v *QueryFragmentBeingsAnimal) GetId() string { return v.Id }
+
+func __unmarshalQueryFragmentBeingsBeing(b []byte, v *QueryFragmentBeingsBeing) error {
+	if string(b) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(b, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "User":
+		*v = new(QueryFragmentBeingsUser)
+		return json.Unmarshal(b, *v)
+	case "Animal":
+		*v = new(QueryFragmentBeingsAnimal)
+		return json.Unmarshal(b, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Being.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for QueryFragmentBeingsBeing: "%v"`, tn.TypeName)
+	}
+}
+
+func __marshalQueryFragmentBeingsBeing(v *QueryFragmentBeingsBeing) ([]byte, error) {
+
+	var typename string
+	switch v := (*v).(type) {
+	case *QueryFragmentBeingsUser:
+		typename = "User"
+
+		premarshaled, err := v.__premarshalJSON()
+		if err != nil {
+			return nil, err
+		}
+		result := struct {
+			TypeName string `json:"__typename"`
+			*__premarshalQueryFragmentBeingsUser
+		}{typename, premarshaled}
+		return json.Marshal(result)
+	case *QueryFragmentBeingsAnimal:
+		typename = "Animal"
+
+		premarshaled, err := v.__premarshalJSON()
+		if err != nil {
+			return nil, err
+		}
+		result := struct {
+			TypeName string `json:"__typename"`
+			*__premarshalQueryFragmentBeingsAnimal
+		}{typename, premarshaled}
+		return json.Marshal(result)
+	case nil:
+		return []byte("null"), nil
+	default:
+		return nil, fmt.Errorf(
+			`Unexpected concrete type for QueryFragmentBeingsBeing: "%T"`, v)
+	}
+}
+
+// QueryFragmentBeingsUser includes the requested fields of the GraphQL type User.
+type QueryFragmentBeingsUser struct {
+	Typename             string `json:"__typename"`
+	Id                   string `json:"id"`
+	InnerLuckyFieldsUser `json:"-"`
+}
+
+func (v *QueryFragmentBeingsUser) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*QueryFragmentBeingsUser
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.QueryFragmentBeingsUser = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(
+		b, &v.InnerLuckyFieldsUser)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+type __premarshalQueryFragmentBeingsUser struct {
+	Typename string `json:"__typename"`
+
+	Id string `json:"id"`
+
+	LuckyNumber int `json:"luckyNumber"`
+}
+
+func (v *QueryFragmentBeingsUser) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *QueryFragmentBeingsUser) __premarshalJSON() (*__premarshalQueryFragmentBeingsUser, error) {
+	var retval __premarshalQueryFragmentBeingsUser
+
+	retval.Typename = v.Typename
+	retval.Id = v.Id
+	retval.LuckyNumber = v.InnerLuckyFieldsUser.LuckyNumber
+	return &retval, nil
+}
+
 type Species string
 
 const (
@@ -679,6 +1157,11 @@ func (v *__queryWithCustomMarshalSliceInput) __premarshalJSON() (*__premarshal__
 	return &retval, nil
 }
 
+// __queryWithFlattenInput is used internally by genqlient
+type __queryWithFlattenInput struct {
+	Ids []string `json:"ids"`
+}
+
 // __queryWithFragmentsInput is used internally by genqlient
 type __queryWithFragmentsInput struct {
 	Ids []string `json:"ids"`
@@ -2678,3 +3161,66 @@ fragment MoreUserFields on User {
 	)
 	return &retval, err
 }
+
+func queryWithFlatten(
+	ctx context.Context,
+	client graphql.Client,
+	ids []string,
+) (*QueryFragment, error) {
+	__input := __queryWithFlattenInput{
+		Ids: ids,
+	}
+	var err error
+
+	var retval QueryFragment
+	err = client.MakeRequest(
+		ctx,
+		"queryWithFlatten",
+		`
+query queryWithFlatten ($ids: [ID!]!) {
+	... QueryFragment
+}
+fragment QueryFragment on Query {
+	beings(ids: $ids) {
+		__typename
+		id
+		... FlattenedUserFields
+		... on Animal {
+			owner {
+				__typename
+				... BeingFields
+			}
+		}
+	}
+}
+fragment FlattenedUserFields on User {
+	... FlattenedLuckyFields
+}
+fragment BeingFields on Being {
+	... InnerBeingFields
+}
+fragment FlattenedLuckyFields on Lucky {
+	... InnerLuckyFields
+}
+fragment InnerBeingFields on Being {
+	id
+	name
+	... on User {
+		friends {
+			... FriendsFields
+		}
+	}
+}
+fragment InnerLuckyFields on Lucky {
+	luckyNumber
+}
+fragment FriendsFields on User {
+	id
+	name
+}
+`,
+		&retval,
+		&__input,
+	)
+	return &retval, err
+}
diff --git a/internal/integration/schema.graphql b/internal/integration/schema.graphql
index a9e581c..6d91862 100644
--- a/internal/integration/schema.graphql
+++ b/internal/integration/schema.graphql
@@ -18,6 +18,7 @@ type User implements Being & Lucky {
   luckyNumber: Int
   hair: Hair
   birthdate: Date
+  friends: [User!]!
 }
 
 type Hair { color: String }   # silly name to confuse the name-generator
diff --git a/internal/integration/server/gqlgen_exec.go b/internal/integration/server/gqlgen_exec.go
index 6a4cf24..6b7e775 100644
--- a/internal/integration/server/gqlgen_exec.go
+++ b/internal/integration/server/gqlgen_exec.go
@@ -72,6 +72,7 @@ type ComplexityRoot struct {
 
 	User struct {
 		Birthdate   func(childComplexity int) int
+		Friends     func(childComplexity int) int
 		Hair        func(childComplexity int) int
 		ID          func(childComplexity int) int
 		LuckyNumber func(childComplexity int) int
@@ -260,6 +261,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.User.Birthdate(childComplexity), true
 
+	case "User.friends":
+		if e.complexity.User.Friends == nil {
+			break
+		}
+
+		return e.complexity.User.Friends(childComplexity), true
+
 	case "User.hair":
 		if e.complexity.User.Hair == nil {
 			break
@@ -358,6 +366,7 @@ type User implements Being & Lucky {
   luckyNumber: Int
   hair: Hair
   birthdate: Date
+  friends: [User!]!
 }
 
 type Hair { color: String }   # silly name to confuse the name-generator
@@ -1379,6 +1388,41 @@ func (ec *executionContext) _User_birthdate(ctx context.Context, field graphql.C
 	return ec.marshalODate2ᚖstring(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _User_friends(ctx context.Context, field graphql.CollectedField, obj *User) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "User",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   false,
+		IsResolver: false,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Friends, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]*User)
+	fc.Result = res
+	return ec.marshalNUser2ᚕᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUserᚄ(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -2770,6 +2814,11 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
 			out.Values[i] = ec._User_hair(ctx, field, obj)
 		case "birthdate":
 			out.Values[i] = ec._User_birthdate(ctx, field, obj)
+		case "friends":
+			out.Values[i] = ec._User_friends(ctx, field, obj)
+			if out.Values[i] == graphql.Null {
+				invalids++
+			}
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
diff --git a/internal/integration/server/gqlgen_models.go b/internal/integration/server/gqlgen_models.go
index b74a709..73ae392 100644
--- a/internal/integration/server/gqlgen_models.go
+++ b/internal/integration/server/gqlgen_models.go
@@ -40,6 +40,7 @@ type User struct {
 	LuckyNumber *int    `json:"luckyNumber"`
 	Hair        *Hair   `json:"hair"`
 	Birthdate   *string `json:"birthdate"`
+	Friends     []*User `json:"friends"`
 }
 
 func (User) IsBeing() {}
diff --git a/internal/integration/server/server.go b/internal/integration/server/server.go
index d0412b0..6988356 100644
--- a/internal/integration/server/server.go
+++ b/internal/integration/server/server.go
@@ -21,6 +21,11 @@ var users = []*User{
 	{ID: "2", Name: "Raven", LuckyNumber: intptr(-1), Hair: nil},
 }
 
+func init() {
+	users[0].Friends = []*User{users[1]} // (obviously a lie, but)
+	users[1].Friends = users             // try to crash the system
+}
+
 var animals = []*Animal{
 	{
 		ID: "3", Name: "Fido", Species: SpeciesDog, Owner: userByID("1"),


Test patch:
diff --git a/generate/testdata/errors/FlattenField.graphql b/generate/testdata/errors/FlattenField.graphql
new file mode 100644
index 0000000..8d32f10
--- /dev/null
+++ b/generate/testdata/errors/FlattenField.graphql
@@ -0,0 +1,6 @@
+query FlattenField {
+  # @genqlient(flatten: true)
+  t {
+    f
+  }
+}
diff --git a/generate/testdata/errors/FlattenField.schema.graphql b/generate/testdata/errors/FlattenField.schema.graphql
new file mode 100644
index 0000000..0977d98
--- /dev/null
+++ b/generate/testdata/errors/FlattenField.schema.graphql
@@ -0,0 +1,2 @@
+type Query { t: T }
+type T { f: String }
diff --git a/generate/testdata/errors/FlattenImplementation.graphql b/generate/testdata/errors/FlattenImplementation.graphql
new file mode 100644
index 0000000..901bc53
--- /dev/null
+++ b/generate/testdata/errors/FlattenImplementation.graphql
@@ -0,0 +1,7 @@
+fragment F on T { f }
+query FlattenImplementation {
+  # @genqlient(flatten: true)
+  i {
+    ...F
+  }
+}
diff --git a/generate/testdata/errors/FlattenImplementation.schema.graphql b/generate/testdata/errors/FlattenImplementation.schema.graphql
new file mode 100644
index 0000000..e5371a6
--- /dev/null
+++ b/generate/testdata/errors/FlattenImplementation.schema.graphql
@@ -0,0 +1,3 @@
+type Query { i: I }
+interface I { f: String }
+type T implements I { f: String }
diff --git a/generate/testdata/queries/Flatten.graphql b/generate/testdata/queries/Flatten.graphql
new file mode 100644
index 0000000..b929aee
--- /dev/null
+++ b/generate/testdata/queries/Flatten.graphql
@@ -0,0 +1,42 @@
+# @genqlient(flatten: true)
+fragment QueryFragment on Query {
+  ...InnerQueryFragment
+}
+
+fragment InnerQueryFragment on Query {
+  # @genqlient(flatten: true)
+  randomVideo {
+    ...VideoFields
+  }
+  # @genqlient(flatten: true)
+  randomItem {
+    ...ContentFields
+  }
+  # @genqlient(flatten: true)
+  otherVideo: randomVideo {
+    ...ContentFields
+  }
+}
+
+fragment VideoFields on Video {
+  id
+  parent {
+    # @genqlient(flatten: true)
+    videoChildren {
+      ...ChildVideoFields
+    }
+  }
+}
+
+fragment ChildVideoFields on Video {
+  id name
+}
+
+fragment ContentFields on Content {
+  name url
+}
+
+# @genqlient(flatten: true)
+query ComplexNamedFragments {
+  ...QueryFragment
+}
diff --git a/generate/testdata/queries/schema.graphql b/generate/testdata/queries/schema.graphql
index d0e6908..4e951bb 100644
--- a/generate/testdata/queries/schema.graphql
+++ b/generate/testdata/queries/schema.graphql
@@ -131,6 +131,7 @@ type Topic implements Content {
   parent: Topic
   url: String!
   children: [Content!]!
+  videoChildren: [Video!]!
   schoolGrade: String
 }
 
@@ -162,6 +163,7 @@ type Query {
   root: Topic!
   randomItem: Content!
   randomLeaf: LeafContent!
+  randomVideo: Video!
   convert(dt: DateTime!, tz: String): DateTime!
   maybeConvert(dt: DateTime, tz: String): DateTime
   getJunk: Junk
diff --git a/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.go b/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.go
new file mode 100644
index 0000000..523b5d6
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.go
@@ -0,0 +1,297 @@
+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"
+)
+
+// ChildVideoFields includes the GraphQL fields of Video requested by the fragment ChildVideoFields.
+type ChildVideoFields struct {
+	// ID is documented in the Content interface.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// ContentFields includes the GraphQL fields of Content requested by the fragment ContentFields.
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+//
+// ContentFields is implemented by the following types:
+// ContentFieldsArticle
+// ContentFieldsVideo
+// ContentFieldsTopic
+type ContentFields interface {
+	implementsGraphQLInterfaceContentFields()
+	// GetName returns the interface-field "name" from its implementation.
+	GetName() string
+	// GetUrl returns the interface-field "url" from its implementation.
+	GetUrl() string
+}
+
+func (v *ContentFieldsArticle) implementsGraphQLInterfaceContentFields() {}
+
+// GetName is a part of, and documented with, the interface ContentFields.
+func (v *ContentFieldsArticle) GetName() string { return v.Name }
+
+// GetUrl is a part of, and documented with, the interface ContentFields.
+func (v *ContentFieldsArticle) GetUrl() string { return v.Url }
+
+func (v *ContentFieldsVideo) implementsGraphQLInterfaceContentFields() {}
+
+// GetName is a part of, and documented with, the interface ContentFields.
+func (v *ContentFieldsVideo) GetName() string { return v.Name }
+
+// GetUrl is a part of, and documented with, the interface ContentFields.
+func (v *ContentFieldsVideo) GetUrl() string { return v.Url }
+
+func (v *ContentFieldsTopic) implementsGraphQLInterfaceContentFields() {}
+
+// GetName is a part of, and documented with, the interface ContentFields.
+func (v *ContentFieldsTopic) GetName() string { return v.Name }
+
+// GetUrl is a part of, and documented with, the interface ContentFields.
+func (v *ContentFieldsTopic) GetUrl() string { return v.Url }
+
+func __unmarshalContentFields(b []byte, v *ContentFields) error {
+	if string(b) == "null" {
+		return nil
+	}
+
+	var tn struct {
+		TypeName string `json:"__typename"`
+	}
+	err := json.Unmarshal(b, &tn)
+	if err != nil {
+		return err
+	}
+
+	switch tn.TypeName {
+	case "Article":
+		*v = new(ContentFieldsArticle)
+		return json.Unmarshal(b, *v)
+	case "Video":
+		*v = new(ContentFieldsVideo)
+		return json.Unmarshal(b, *v)
+	case "Topic":
+		*v = new(ContentFieldsTopic)
+		return json.Unmarshal(b, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for ContentFields: "%v"`, tn.TypeName)
+	}
+}
+
+func __marshalContentFields(v *ContentFields) ([]byte, error) {
+
+	var typename string
+	switch v := (*v).(type) {
+	case *ContentFieldsArticle:
+		typename = "Article"
+
+		result := struct {
+			TypeName string `json:"__typename"`
+			*ContentFieldsArticle
+		}{typename, v}
+		return json.Marshal(result)
+	case *ContentFieldsVideo:
+		typename = "Video"
+
+		result := struct {
+			TypeName string `json:"__typename"`
+			*ContentFieldsVideo
+		}{typename, v}
+		return json.Marshal(result)
+	case *ContentFieldsTopic:
+		typename = "Topic"
+
+		result := struct {
+			TypeName string `json:"__typename"`
+			*ContentFieldsTopic
+		}{typename, v}
+		return json.Marshal(result)
+	case nil:
+		return []byte("null"), nil
+	default:
+		return nil, fmt.Errorf(
+			`Unexpected concrete type for ContentFields: "%T"`, v)
+	}
+}
+
+// ContentFields includes the GraphQL fields of Article requested by the fragment ContentFields.
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type ContentFieldsArticle struct {
+	Name string `json:"name"`
+	Url  string `json:"url"`
+}
+
+// ContentFields includes the GraphQL fields of Topic requested by the fragment ContentFields.
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type ContentFieldsTopic struct {
+	Name string `json:"name"`
+	Url  string `json:"url"`
+}
+
+// ContentFields includes the GraphQL fields of Video requested by the fragment ContentFields.
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type ContentFieldsVideo struct {
+	Name string `json:"name"`
+	Url  string `json:"url"`
+}
+
+// InnerQueryFragment includes the GraphQL fields of Query requested by the fragment InnerQueryFragment.
+// The GraphQL type's documentation follows.
+//
+// Query's description is probably ignored by almost all callers.
+type InnerQueryFragment struct {
+	RandomVideo VideoFields        `json:"randomVideo"`
+	RandomItem  ContentFields      `json:"-"`
+	OtherVideo  ContentFieldsVideo `json:"otherVideo"`
+}
+
+func (v *InnerQueryFragment) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*InnerQueryFragment
+		RandomItem json.RawMessage `json:"randomItem"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.InnerQueryFragment = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		dst := &v.RandomItem
+		src := firstPass.RandomItem
+		if len(src) != 0 && string(src) != "null" {
+			err = __unmarshalContentFields(
+				src, dst)
+			if err != nil {
+				return fmt.Errorf(
+					"Unable to unmarshal InnerQueryFragment.RandomItem: %w", err)
+			}
+		}
+	}
+	return nil
+}
+
+type __premarshalInnerQueryFragment struct {
+	RandomVideo VideoFields `json:"randomVideo"`
+
+	RandomItem json.RawMessage `json:"randomItem"`
+
+	OtherVideo ContentFieldsVideo `json:"otherVideo"`
+}
+
+func (v *InnerQueryFragment) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *InnerQueryFragment) __premarshalJSON() (*__premarshalInnerQueryFragment, error) {
+	var retval __premarshalInnerQueryFragment
+
+	retval.RandomVideo = v.RandomVideo
+	{
+
+		dst := &retval.RandomItem
+		src := v.RandomItem
+		var err error
+		*dst, err = __marshalContentFields(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"Unable to marshal InnerQueryFragment.RandomItem: %w", err)
+		}
+	}
+	retval.OtherVideo = v.OtherVideo
+	return &retval, 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"`
+	Parent VideoFieldsParentTopic `json:"parent"`
+}
+
+// VideoFieldsParentTopic includes the requested fields of the GraphQL type Topic.
+type VideoFieldsParentTopic struct {
+	VideoChildren []ChildVideoFields `json:"videoChildren"`
+}
+
+func ComplexNamedFragments(
+	client graphql.Client,
+) (*InnerQueryFragment, error) {
+	var err error
+
+	var retval InnerQueryFragment
+	err = client.MakeRequest(
+		nil,
+		"ComplexNamedFragments",
+		`
+query ComplexNamedFragments {
+	... QueryFragment
+}
+fragment QueryFragment on Query {
+	... InnerQueryFragment
+}
+fragment InnerQueryFragment on Query {
+	randomVideo {
+		... VideoFields
+	}
+	randomItem {
+		__typename
+		... ContentFields
+	}
+	otherVideo: randomVideo {
+		... ContentFields
+	}
+}
+fragment VideoFields on Video {
+	id
+	parent {
+		videoChildren {
+			... ChildVideoFields
+		}
+	}
+}
+fragment ContentFields on Content {
+	name
+	url
+}
+fragment ChildVideoFields on Video {
+	id
+	name
+}
+`,
+		&retval,
+		nil,
+	)
+	return &retval, err
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.json b/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.json
new file mode 100644
index 0000000..1daf019
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-Flatten.graphql-Flatten.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "ComplexNamedFragments",
+      "query": "\nquery ComplexNamedFragments {\n\t... QueryFragment\n}\nfragment QueryFragment on Query {\n\t... InnerQueryFragment\n}\nfragment InnerQueryFragment on Query {\n\trandomVideo {\n\t\t... VideoFields\n\t}\n\trandomItem {\n\t\t__typename\n\t\t... ContentFields\n\t}\n\totherVideo: randomVideo {\n\t\t... ContentFields\n\t}\n}\nfragment VideoFields on Video {\n\tid\n\tparent {\n\t\tvideoChildren {\n\t\t\t... ChildVideoFields\n\t\t}\n\t}\n}\nfragment ContentFields on Content {\n\tname\n\turl\n}\nfragment ChildVideoFields on Video {\n\tid\n\tname\n}\n",
+      "sourceLocation": "testdata/queries/Flatten.graphql"
+    }
+  ]
+}
diff --git a/generate/testdata/snapshots/TestGenerateErrors-FlattenField-graphql b/generate/testdata/snapshots/TestGenerateErrors-FlattenField-graphql
new file mode 100644
index 0000000..d1606af
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerateErrors-FlattenField-graphql
@@ -0,0 +1,1 @@
+testdata/errors/FlattenField.graphql:3: flatten is not yet supported for fields (only fragment spreads)
diff --git a/generate/testdata/snapshots/TestGenerateErrors-FlattenImplementation-graphql b/generate/testdata/snapshots/TestGenerateErrors-FlattenImplementation-graphql
new file mode 100644
index 0000000..599292b
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerateErrors-FlattenImplementation-graphql
@@ -0,0 +1,1 @@
+testdata/errors/FlattenImplementation.graphql:4: flatten is not allowed for fields with fragment-spreads unless the field-type implements the fragment-type; field-type I does not implement fragment-type T
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index b0fd528..55162ad 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -554,6 +554,116 @@ func TestNamedFragments(t *testing.T) {
 	assert.Nil(t, resp.Beings[2])
 }
 
+func TestFlatten(t *testing.T) {
+	_ = `# @genqlient
+	# @genqlient(flatten: true)
+	fragment BeingFields on Being {
+		...InnerBeingFields
+	}
+
+	fragment InnerBeingFields on Being {
+		id
+		name
+		... on User {
+			# @genqlient(flatten: true)
+			friends {
+				...FriendsFields
+			}
+		}
+	}
+
+	fragment FriendsFields on User {
+		id
+		name
+	}
+
+	# @genqlient(flatten: true)
+	fragment FlattenedUserFields on User {
+		...FlattenedLuckyFields
+	}
+
+	# @genqlient(flatten: true)
+	fragment FlattenedLuckyFields on Lucky {
+		...InnerLuckyFields
+	}
+
+	fragment InnerLuckyFields on Lucky {
+		luckyNumber
+	}
+	
+	fragment QueryFragment on Query {
+		beings(ids: $ids) {
+			__typename id
+			...FlattenedUserFields
+			... on Animal {
+				# @genqlient(flatten: true)
+				owner {
+					...BeingFields
+				}
+			}
+		}
+	}
+
+	# @genqlient(flatten: true)
+	query queryWithFlatten(
+		$ids: [ID!]!,
+	) {
+		...QueryFragment
+	}`
+
+	ctx := context.Background()
+	server := server.RunServer()
+	defer server.Close()
+	client := newRoundtripClient(t, server.URL)
+
+	resp, err := queryWithFlatten(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 we need to cast for)
+
+	user, ok := resp.Beings[0].(*QueryFragmentBeingsUser)
+	require.Truef(t, ok, "got %T, not User", resp.Beings[0])
+	assert.Equal(t, "1", user.Id)
+	assert.Equal(t, 17, user.InnerLuckyFieldsUser.LuckyNumber)
+
+	// Animal has, in total, the fields:
+	//	__typename
+	//	id
+	//	owner { id name ... on User { friends { id name } } }
+	assert.Equal(t, "Animal", resp.Beings[1].GetTypename())
+	assert.Equal(t, "3", resp.Beings[1].GetId())
+	// (owner.* we have to cast for)
+
+	animal, ok := resp.Beings[1].(*QueryFragmentBeingsAnimal)
+	require.Truef(t, ok, "got %T, not Animal", resp.Beings[1])
+	assert.Equal(t, "3", animal.Id)
+	// on AnimalFields:
+	assert.Equal(t, "1", animal.Owner.GetId())
+	assert.Equal(t, "Yours Truly", animal.Owner.GetName())
+	// (friends.* we have to cast for, again)
+
+	owner, ok := animal.Owner.(*InnerBeingFieldsUser)
+	require.Truef(t, ok, "got %T, not User", animal.Owner)
+	assert.Equal(t, "1", owner.Id)
+	assert.Equal(t, "Yours Truly", owner.Name)
+	assert.Len(t, owner.Friends, 1)
+	assert.Equal(t, "2", owner.Friends[0].Id)
+	assert.Equal(t, "Raven", owner.Friends[0].Name)
+
+	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