Repository: Khan/genqlient
Pull request #309 (2024-02-14 19:31:44)

Support more valid graphql file extensions
There are two other commonly used graphql file extension we should support
.gql - used by apollo and other client library to differentiate query from schema
.graphqls - used by gqlgen and other server library to differentiate schema from query

https://github.com/apollographql/apollo-tooling/blob/a778ca162bdd20be9e60a914887302390c274644/packages/apollo/src/commands/client/download-schema.ts#L19


I have:
- [x] Written a clear PR title and description (above)
- [x] Signed the [Khan Academy CLA](https://www.khanacademy.org/r/cla)
- [x] Added tests covering my changes, if applicable
- [x] Included a link to the issue fixed, if applicable
- [x] Included documentation, for new features
- [x] Added an entry to the changelog


benjaminjkraft: This looks good to me! We can merge once I get confirmation from the CLA person on CLAs and get a chance to look slightly closer at the lint issue (I doubt it's related)
benjaminjkraft: CLA confirmed. Lint is fixed on main, but either I don't have permission to merge into your branch or I don't know how to use that permission from CLI, so I think you'll need to do that. Once you do we should be good!
zzh8829: rebased

Patch:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index bec20ef..be0c36f 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -26,10 +26,12 @@ When releasing a new version:
 
 - The new `optional: generic` allows using a generic type to represent optionality. See the [documentation](genqlient.yaml) for details.
 - For schemas with enum values that differ only in casing, it's now possible to disable smart-casing in genqlient.yaml; see the [documentation](genqlient.yaml) for `casing` for details.
+- Support .graphqls and .gql file extensions
 
 ### Bug fixes:
 - The presence of negative pointer directives, i.e., `# @genqlient(pointer: false)` are now respected even in the when `optional: pointer` is set in the configuration file.
 - Made name collisions between query/mutation arguments and local function variables less likely.
+- Fix generation issue related to golang type implementation of complex graphql union fragments
 
 ## v0.6.0
 
diff --git a/generate/convert.go b/generate/convert.go
index 3b1f750..d3a9f8d 100644
--- a/generate/convert.go
+++ b/generate/convert.go
@@ -701,6 +701,17 @@ func fragmentMatches(containingTypedef, fragmentTypedef *ast.Definition) bool {
 			return true
 		}
 	}
+
+	// Handle the special case where the fragment is on a union, then the
+	// fragment can match any of the types in the union.
+	if fragmentTypedef.Kind == ast.Union {
+		for _, typeName := range fragmentTypedef.Types {
+			if typeName == containingTypedef.Name {
+				return true
+			}
+		}
+	}
+
 	return false
 }
 
diff --git a/generate/parse.go b/generate/parse.go
index 4569f25..6ef7656 100644
--- a/generate/parse.go
+++ b/generate/parse.go
@@ -130,7 +130,7 @@ func getQueries(basedir string, globs StringList) (*ast.QueryDocument, error) {
 		}
 
 		switch filepath.Ext(filename) {
-		case ".graphql":
+		case ".graphql", ".graphqls", ".gql":
 			queryDoc, err := getQueriesFromString(string(text), basedir, filename)
 			if err != nil {
 				return nil, err


Test patch:
diff --git a/generate/testdata/queries/ComplexNamedFragments.graphql b/generate/testdata/queries/ComplexNamedFragments.graphql
index 57feb65..12036c8 100644
--- a/generate/testdata/queries/ComplexNamedFragments.graphql
+++ b/generate/testdata/queries/ComplexNamedFragments.graphql
@@ -47,3 +47,34 @@ fragment ContentFields on Content {
 query ComplexNamedFragments {
   ... on Query { ...QueryFragment }
 }
+
+## two fragments of different types with fields containing the same inline named fragment of a union
+fragment SimpleLeafContent on LeafContent {
+  ... on Article {
+    id
+  }
+  ... on Video {
+    id
+  }
+}
+
+fragment UserLastContent on User {
+  lastContent {
+    ... SimpleLeafContent
+  }
+}
+
+fragment TopicNewestContent on Topic {
+  newestContent {
+    ... SimpleLeafContent
+  }
+}
+
+query ComplexNamedFragmentsWithInlineUnion {
+  user {
+    ...UserLastContent
+  }
+  root {
+    ...TopicNewestContent
+  }
+}
diff --git a/generate/testdata/queries/schema.graphql b/generate/testdata/queries/schema.graphql
index 17db360..eb71bdd 100644
--- a/generate/testdata/queries/schema.graphql
+++ b/generate/testdata/queries/schema.graphql
@@ -69,6 +69,8 @@ type User {
   pokemon: [Pokemon!]
   greeting: Clip
   birthdate: Date
+
+  lastContent: LeafContent
 }
 
 """An audio clip, such as of a user saying hello."""
@@ -141,6 +143,8 @@ type Topic implements Content {
   schoolGrade: String
   next: Topic
   related: [Topic!]
+
+  newestContent: LeafContent
 }
 
 input RecursiveInput {
diff --git a/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go
index 5e7350c..7888b2e 100644
--- a/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go
@@ -113,6 +113,159 @@ func (v *ComplexNamedFragmentsResponse) __premarshalJSON() (*__premarshalComplex
 	return &retval, nil
 }
 
+// ComplexNamedFragmentsWithInlineUnionResponse is returned by ComplexNamedFragmentsWithInlineUnion on success.
+type ComplexNamedFragmentsWithInlineUnionResponse struct {
+	// user looks up a user by some stuff.
+	//
+	// See UserQueryInput for what stuff is supported.
+	// If query is null, returns the current user.
+	User ComplexNamedFragmentsWithInlineUnionUser      `json:"user"`
+	Root ComplexNamedFragmentsWithInlineUnionRootTopic `json:"root"`
+}
+
+// GetUser returns ComplexNamedFragmentsWithInlineUnionResponse.User, and is useful for accessing the field via an interface.
+func (v *ComplexNamedFragmentsWithInlineUnionResponse) GetUser() ComplexNamedFragmentsWithInlineUnionUser {
+	return v.User
+}
+
+// GetRoot returns ComplexNamedFragmentsWithInlineUnionResponse.Root, and is useful for accessing the field via an interface.
+func (v *ComplexNamedFragmentsWithInlineUnionResponse) GetRoot() ComplexNamedFragmentsWithInlineUnionRootTopic {
+	return v.Root
+}
+
+// ComplexNamedFragmentsWithInlineUnionRootTopic includes the requested fields of the GraphQL type Topic.
+type ComplexNamedFragmentsWithInlineUnionRootTopic struct {
+	TopicNewestContent `json:"-"`
+}
+
+// GetNewestContent returns ComplexNamedFragmentsWithInlineUnionRootTopic.NewestContent, and is useful for accessing the field via an interface.
+func (v *ComplexNamedFragmentsWithInlineUnionRootTopic) GetNewestContent() TopicNewestContentNewestContentLeafContent {
+	return v.TopicNewestContent.NewestContent
+}
+
+func (v *ComplexNamedFragmentsWithInlineUnionRootTopic) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*ComplexNamedFragmentsWithInlineUnionRootTopic
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.ComplexNamedFragmentsWithInlineUnionRootTopic = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(
+		b, &v.TopicNewestContent)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+type __premarshalComplexNamedFragmentsWithInlineUnionRootTopic struct {
+	NewestContent json.RawMessage `json:"newestContent"`
+}
+
+func (v *ComplexNamedFragmentsWithInlineUnionRootTopic) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *ComplexNamedFragmentsWithInlineUnionRootTopic) __premarshalJSON() (*__premarshalComplexNamedFragmentsWithInlineUnionRootTopic, error) {
+	var retval __premarshalComplexNamedFragmentsWithInlineUnionRootTopic
+
+	{
+
+		dst := &retval.NewestContent
+		src := v.TopicNewestContent.NewestContent
+		var err error
+		*dst, err = __marshalTopicNewestContentNewestContentLeafContent(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"unable to marshal ComplexNamedFragmentsWithInlineUnionRootTopic.TopicNewestContent.NewestContent: %w", err)
+		}
+	}
+	return &retval, nil
+}
+
+// ComplexNamedFragmentsWithInlineUnionUser includes the requested fields of the GraphQL type User.
+// The GraphQL type's documentation follows.
+//
+// A User is a user!
+type ComplexNamedFragmentsWithInlineUnionUser struct {
+	UserLastContent `json:"-"`
+}
+
+// GetLastContent returns ComplexNamedFragmentsWithInlineUnionUser.LastContent, and is useful for accessing the field via an interface.
+func (v *ComplexNamedFragmentsWithInlineUnionUser) GetLastContent() UserLastContentLastContentLeafContent {
+	return v.UserLastContent.LastContent
+}
+
+func (v *ComplexNamedFragmentsWithInlineUnionUser) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*ComplexNamedFragmentsWithInlineUnionUser
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.ComplexNamedFragmentsWithInlineUnionUser = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(
+		b, &v.UserLastContent)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+type __premarshalComplexNamedFragmentsWithInlineUnionUser struct {
+	LastContent json.RawMessage `json:"lastContent"`
+}
+
+func (v *ComplexNamedFragmentsWithInlineUnionUser) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *ComplexNamedFragmentsWithInlineUnionUser) __premarshalJSON() (*__premarshalComplexNamedFragmentsWithInlineUnionUser, error) {
+	var retval __premarshalComplexNamedFragmentsWithInlineUnionUser
+
+	{
+
+		dst := &retval.LastContent
+		src := v.UserLastContent.LastContent
+		var err error
+		*dst, err = __marshalUserLastContentLastContentLeafContent(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"unable to marshal ComplexNamedFragmentsWithInlineUnionUser.UserLastContent.LastContent: %w", err)
+		}
+	}
+	return &retval, nil
+}
+
 // ContentFields includes the GraphQL fields of Content requested by the fragment ContentFields.
 // The GraphQL type's documentation follows.
 //
@@ -1649,70 +1802,141 @@ func (v *QueryFragment) __premarshalJSON() (*__premarshalQueryFragment, error) {
 	return &retval, nil
 }
 
-// VideoFields includes the GraphQL fields of Video requested by the fragment VideoFields.
-type VideoFields struct {
+// # two fragments of different types with fields containing the same inline named fragment of a union
+//
+// SimpleLeafContent is implemented by the following types:
+// SimpleLeafContentArticle
+// SimpleLeafContentVideo
+type SimpleLeafContent interface {
+	implementsGraphQLInterfaceSimpleLeafContent()
+}
+
+func (v *SimpleLeafContentArticle) implementsGraphQLInterfaceSimpleLeafContent() {}
+func (v *SimpleLeafContentVideo) implementsGraphQLInterfaceSimpleLeafContent()   {}
+
+func __unmarshalSimpleLeafContent(b []byte, v *SimpleLeafContent) 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(SimpleLeafContentArticle)
+		return json.Unmarshal(b, *v)
+	case "Video":
+		*v = new(SimpleLeafContentVideo)
+		return json.Unmarshal(b, *v)
+	case "":
+		return fmt.Errorf(
+			"response was missing LeafContent.__typename")
+	default:
+		return fmt.Errorf(
+			`unexpected concrete type for SimpleLeafContent: "%v"`, tn.TypeName)
+	}
+}
+
+func __marshalSimpleLeafContent(v *SimpleLeafContent) ([]byte, error) {
+
+	var typename string
+	switch v := (*v).(type) {
+	case *SimpleLeafContentArticle:
+		typename = "Article"
+
+		result := struct {
+			TypeName string `json:"__typename"`
+			*SimpleLeafContentArticle
+		}{typename, v}
+		return json.Marshal(result)
+	case *SimpleLeafContentVideo:
+		typename = "Video"
+
+		result := struct {
+			TypeName string `json:"__typename"`
+			*SimpleLeafContentVideo
+		}{typename, v}
+		return json.Marshal(result)
+	case nil:
+		return []byte("null"), nil
+	default:
+		return nil, fmt.Errorf(
+			`unexpected concrete type for SimpleLeafContent: "%T"`, v)
+	}
+}
+
+// # two fragments of different types with fields containing the same inline named fragment of a union
+type SimpleLeafContentArticle 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"`
-	ContentFieldsVideo `json:"-"`
+	Id testutil.ID `json:"id"`
 }
 
-// GetId returns VideoFields.Id, and is useful for accessing the field via an interface.
-func (v *VideoFields) GetId() testutil.ID { return v.Id }
+// GetId returns SimpleLeafContentArticle.Id, and is useful for accessing the field via an interface.
+func (v *SimpleLeafContentArticle) GetId() testutil.ID { return v.Id }
 
-// GetName returns VideoFields.Name, and is useful for accessing the field via an interface.
-func (v *VideoFields) GetName() string { return v.Name }
+// # two fragments of different types with fields containing the same inline named fragment of a union
+type SimpleLeafContentVideo struct {
+	// ID is documented in the Content interface.
+	Id testutil.ID `json:"id"`
+}
 
-// GetUrl returns VideoFields.Url, and is useful for accessing the field via an interface.
-func (v *VideoFields) GetUrl() string { return v.Url }
+// GetId returns SimpleLeafContentVideo.Id, and is useful for accessing the field via an interface.
+func (v *SimpleLeafContentVideo) GetId() testutil.ID { return v.Id }
 
-// GetDuration returns VideoFields.Duration, and is useful for accessing the field via an interface.
-func (v *VideoFields) GetDuration() int { return v.Duration }
+// TopicNewestContent includes the GraphQL fields of Topic requested by the fragment TopicNewestContent.
+type TopicNewestContent struct {
+	NewestContent TopicNewestContentNewestContentLeafContent `json:"-"`
+}
 
-// GetThumbnail returns VideoFields.Thumbnail, and is useful for accessing the field via an interface.
-func (v *VideoFields) GetThumbnail() VideoFieldsThumbnail { return v.Thumbnail }
+// GetNewestContent returns TopicNewestContent.NewestContent, and is useful for accessing the field via an interface.
+func (v *TopicNewestContent) GetNewestContent() TopicNewestContentNewestContentLeafContent {
+	return v.NewestContent
+}
 
-func (v *VideoFields) UnmarshalJSON(b []byte) error {
+func (v *TopicNewestContent) UnmarshalJSON(b []byte) error {
 
 	if string(b) == "null" {
 		return nil
 	}
 
 	var firstPass struct {
-		*VideoFields
+		*TopicNewestContent
+		NewestContent json.RawMessage `json:"newestContent"`
 		graphql.NoUnmarshalJSON
 	}
-	firstPass.VideoFields = v
+	firstPass.TopicNewestContent = v
 
 	err := json.Unmarshal(b, &firstPass)
 	if err != nil {
 		return err
 	}
 
-	err = json.Unmarshal(
-		b, &v.ContentFieldsVideo)
-	if err != nil {
-		return err
+	{
+		dst := &v.NewestContent
+		src := firstPass.NewestContent
+		if len(src) != 0 && string(src) != "null" {
+			err = __unmarshalTopicNewestContentNewestContentLeafContent(
+				src, dst)
+			if err != nil {
+				return fmt.Errorf(
+					"unable to unmarshal TopicNewestContent.NewestContent: %w", err)
+			}
+		}
 	}
 	return nil
 }
 
-type __premarshalVideoFields struct {
-	Id testutil.ID `json:"id"`
-
-	Name string `json:"name"`
-
-	Url string `json:"url"`
-
-	Duration int `json:"duration"`
-
-	Thumbnail VideoFieldsThumbnail `json:"thumbnail"`
+type __premarshalTopicNewestContent struct {
+	NewestContent json.RawMessage `json:"newestContent"`
 }
 
-func (v *VideoFields) MarshalJSON() ([]byte, error) {
+func (v *TopicNewestContent) MarshalJSON() ([]byte, error) {
 	premarshaled, err := v.__premarshalJSON()
 	if err != nil {
 		return nil, err
@@ -1720,56 +1944,631 @@ func (v *VideoFields) MarshalJSON() ([]byte, error) {
 	return json.Marshal(premarshaled)
 }
 
-func (v *VideoFields) __premarshalJSON() (*__premarshalVideoFields, error) {
-	var retval __premarshalVideoFields
+func (v *TopicNewestContent) __premarshalJSON() (*__premarshalTopicNewestContent, error) {
+	var retval __premarshalTopicNewestContent
 
-	retval.Id = v.Id
-	retval.Name = v.Name
-	retval.Url = v.Url
-	retval.Duration = v.Duration
-	retval.Thumbnail = v.Thumbnail
+	{
+
+		dst := &retval.NewestContent
+		src := v.NewestContent
+		var err error
+		*dst, err = __marshalTopicNewestContentNewestContentLeafContent(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"unable to marshal TopicNewestContent.NewestContent: %w", err)
+		}
+	}
 	return &retval, nil
 }
 
-// VideoFieldsThumbnail includes the requested fields of the GraphQL type Thumbnail.
-type VideoFieldsThumbnail struct {
-	Id testutil.ID `json:"id"`
+// TopicNewestContentNewestContentArticle includes the requested fields of the GraphQL type Article.
+type TopicNewestContentNewestContentArticle struct {
+	Typename                 string `json:"__typename"`
+	SimpleLeafContentArticle `json:"-"`
 }
 
-// GetId returns VideoFieldsThumbnail.Id, and is useful for accessing the field via an interface.
-func (v *VideoFieldsThumbnail) GetId() testutil.ID { return v.Id }
+// GetTypename returns TopicNewestContentNewestContentArticle.Typename, and is useful for accessing the field via an interface.
+func (v *TopicNewestContentNewestContentArticle) GetTypename() string { return v.Typename }
 
-// The query or mutation executed by ComplexNamedFragments.
-const ComplexNamedFragments_Operation = `
-query ComplexNamedFragments {
-	... on Query {
-		... QueryFragment
-	}
-}
-fragment QueryFragment on Query {
-	... InnerQueryFragment
+// GetId returns TopicNewestContentNewestContentArticle.Id, and is useful for accessing the field via an interface.
+func (v *TopicNewestContentNewestContentArticle) GetId() testutil.ID {
+	return v.SimpleLeafContentArticle.Id
 }
-fragment InnerQueryFragment on Query {
-	randomItem {
-		__typename
-		id
-		name
-		... VideoFields
-		... ContentFields
+
+func (v *TopicNewestContentNewestContentArticle) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
 	}
-	randomLeaf {
-		__typename
-		... VideoFields
-		... MoreVideoFields
-		... ContentFields
+
+	var firstPass struct {
+		*TopicNewestContentNewestContentArticle
+		graphql.NoUnmarshalJSON
 	}
-	otherLeaf: randomLeaf {
-		__typename
-		... on Video {
-			... MoreVideoFields
-			... ContentFields
-		}
-		... ContentFields
+	firstPass.TopicNewestContentNewestContentArticle = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(
+		b, &v.SimpleLeafContentArticle)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+type __premarshalTopicNewestContentNewestContentArticle struct {
+	Typename string `json:"__typename"`
+
+	Id testutil.ID `json:"id"`
+}
+
+func (v *TopicNewestContentNewestContentArticle) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *TopicNewestContentNewestContentArticle) __premarshalJSON() (*__premarshalTopicNewestContentNewestContentArticle, error) {
+	var retval __premarshalTopicNewestContentNewestContentArticle
+
+	retval.Typename = v.Typename
+	retval.Id = v.SimpleLeafContentArticle.Id
+	return &retval, nil
+}
+
+// TopicNewestContentNewestContentLeafContent includes the requested fields of the GraphQL interface LeafContent.
+//
+// TopicNewestContentNewestContentLeafContent is implemented by the following types:
+// TopicNewestContentNewestContentArticle
+// TopicNewestContentNewestContentVideo
+// The GraphQL type's documentation follows.
+//
+// LeafContent represents content items that can't have child-nodes.
+type TopicNewestContentNewestContentLeafContent interface {
+	implementsGraphQLInterfaceTopicNewestContentNewestContentLeafContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+	SimpleLeafContent
+}
+
+func (v *TopicNewestContentNewestContentArticle) implementsGraphQLInterfaceTopicNewestContentNewestContentLeafContent() {
+}
+func (v *TopicNewestContentNewestContentVideo) implementsGraphQLInterfaceTopicNewestContentNewestContentLeafContent() {
+}
+
+func __unmarshalTopicNewestContentNewestContentLeafContent(b []byte, v *TopicNewestContentNewestContentLeafContent) 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(TopicNewestContentNewestContentArticle)
+		return json.Unmarshal(b, *v)
+	case "Video":
+		*v = new(TopicNewestContentNewestContentVideo)
+		return json.Unmarshal(b, *v)
+	case "":
+		return fmt.Errorf(
+			"response was missing LeafContent.__typename")
+	default:
+		return fmt.Errorf(
+			`unexpected concrete type for TopicNewestContentNewestContentLeafContent: "%v"`, tn.TypeName)
+	}
+}
+
+func __marshalTopicNewestContentNewestContentLeafContent(v *TopicNewestContentNewestContentLeafContent) ([]byte, error) {
+
+	var typename string
+	switch v := (*v).(type) {
+	case *TopicNewestContentNewestContentArticle:
+		typename = "Article"
+
+		premarshaled, err := v.__premarshalJSON()
+		if err != nil {
+			return nil, err
+		}
+		result := struct {
+			TypeName string `json:"__typename"`
+			*__premarshalTopicNewestContentNewestContentArticle
+		}{typename, premarshaled}
+		return json.Marshal(result)
+	case *TopicNewestContentNewestContentVideo:
+		typename = "Video"
+
+		premarshaled, err := v.__premarshalJSON()
+		if err != nil {
+			return nil, err
+		}
+		result := struct {
+			TypeName string `json:"__typename"`
+			*__premarshalTopicNewestContentNewestContentVideo
+		}{typename, premarshaled}
+		return json.Marshal(result)
+	case nil:
+		return []byte("null"), nil
+	default:
+		return nil, fmt.Errorf(
+			`unexpected concrete type for TopicNewestContentNewestContentLeafContent: "%T"`, v)
+	}
+}
+
+// TopicNewestContentNewestContentVideo includes the requested fields of the GraphQL type Video.
+type TopicNewestContentNewestContentVideo struct {
+	Typename               string `json:"__typename"`
+	SimpleLeafContentVideo `json:"-"`
+}
+
+// GetTypename returns TopicNewestContentNewestContentVideo.Typename, and is useful for accessing the field via an interface.
+func (v *TopicNewestContentNewestContentVideo) GetTypename() string { return v.Typename }
+
+// GetId returns TopicNewestContentNewestContentVideo.Id, and is useful for accessing the field via an interface.
+func (v *TopicNewestContentNewestContentVideo) GetId() testutil.ID {
+	return v.SimpleLeafContentVideo.Id
+}
+
+func (v *TopicNewestContentNewestContentVideo) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*TopicNewestContentNewestContentVideo
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.TopicNewestContentNewestContentVideo = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(
+		b, &v.SimpleLeafContentVideo)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+type __premarshalTopicNewestContentNewestContentVideo struct {
+	Typename string `json:"__typename"`
+
+	Id testutil.ID `json:"id"`
+}
+
+func (v *TopicNewestContentNewestContentVideo) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *TopicNewestContentNewestContentVideo) __premarshalJSON() (*__premarshalTopicNewestContentNewestContentVideo, error) {
+	var retval __premarshalTopicNewestContentNewestContentVideo
+
+	retval.Typename = v.Typename
+	retval.Id = v.SimpleLeafContentVideo.Id
+	return &retval, nil
+}
+
+// UserLastContent includes the GraphQL fields of User requested by the fragment UserLastContent.
+// The GraphQL type's documentation follows.
+//
+// A User is a user!
+type UserLastContent struct {
+	LastContent UserLastContentLastContentLeafContent `json:"-"`
+}
+
+// GetLastContent returns UserLastContent.LastContent, and is useful for accessing the field via an interface.
+func (v *UserLastContent) GetLastContent() UserLastContentLastContentLeafContent {
+	return v.LastContent
+}
+
+func (v *UserLastContent) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*UserLastContent
+		LastContent json.RawMessage `json:"lastContent"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.UserLastContent = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		dst := &v.LastContent
+		src := firstPass.LastContent
+		if len(src) != 0 && string(src) != "null" {
+			err = __unmarshalUserLastContentLastContentLeafContent(
+				src, dst)
+			if err != nil {
+				return fmt.Errorf(
+					"unable to unmarshal UserLastContent.LastContent: %w", err)
+			}
+		}
+	}
+	return nil
+}
+
+type __premarshalUserLastContent struct {
+	LastContent json.RawMessage `json:"lastContent"`
+}
+
+func (v *UserLastContent) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *UserLastContent) __premarshalJSON() (*__premarshalUserLastContent, error) {
+	var retval __premarshalUserLastContent
+
+	{
+
+		dst := &retval.LastContent
+		src := v.LastContent
+		var err error
+		*dst, err = __marshalUserLastContentLastContentLeafContent(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"unable to marshal UserLastContent.LastContent: %w", err)
+		}
+	}
+	return &retval, nil
+}
+
+// UserLastContentLastContentArticle includes the requested fields of the GraphQL type Article.
+type UserLastContentLastContentArticle struct {
+	Typename                 string `json:"__typename"`
+	SimpleLeafContentArticle `json:"-"`
+}
+
+// GetTypename returns UserLastContentLastContentArticle.Typename, and is useful for accessing the field via an interface.
+func (v *UserLastContentLastContentArticle) GetTypename() string { return v.Typename }
+
+// GetId returns UserLastContentLastContentArticle.Id, and is useful for accessing the field via an interface.
+func (v *UserLastContentLastContentArticle) GetId() testutil.ID { return v.SimpleLeafContentArticle.Id }
+
+func (v *UserLastContentLastContentArticle) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*UserLastContentLastContentArticle
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.UserLastContentLastContentArticle = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(
+		b, &v.SimpleLeafContentArticle)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+type __premarshalUserLastContentLastContentArticle struct {
+	Typename string `json:"__typename"`
+
+	Id testutil.ID `json:"id"`
+}
+
+func (v *UserLastContentLastContentArticle) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *UserLastContentLastContentArticle) __premarshalJSON() (*__premarshalUserLastContentLastContentArticle, error) {
+	var retval __premarshalUserLastContentLastContentArticle
+
+	retval.Typename = v.Typename
+	retval.Id = v.SimpleLeafContentArticle.Id
+	return &retval, nil
+}
+
+// UserLastContentLastContentLeafContent includes the requested fields of the GraphQL interface LeafContent.
+//
+// UserLastContentLastContentLeafContent is implemented by the following types:
+// UserLastContentLastContentArticle
+// UserLastContentLastContentVideo
+// The GraphQL type's documentation follows.
+//
+// LeafContent represents content items that can't have child-nodes.
+type UserLastContentLastContentLeafContent interface {
+	implementsGraphQLInterfaceUserLastContentLastContentLeafContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+	SimpleLeafContent
+}
+
+func (v *UserLastContentLastContentArticle) implementsGraphQLInterfaceUserLastContentLastContentLeafContent() {
+}
+func (v *UserLastContentLastContentVideo) implementsGraphQLInterfaceUserLastContentLastContentLeafContent() {
+}
+
+func __unmarshalUserLastContentLastContentLeafContent(b []byte, v *UserLastContentLastContentLeafContent) 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(UserLastContentLastContentArticle)
+		return json.Unmarshal(b, *v)
+	case "Video":
+		*v = new(UserLastContentLastContentVideo)
+		return json.Unmarshal(b, *v)
+	case "":
+		return fmt.Errorf(
+			"response was missing LeafContent.__typename")
+	default:
+		return fmt.Errorf(
+			`unexpected concrete type for UserLastContentLastContentLeafContent: "%v"`, tn.TypeName)
+	}
+}
+
+func __marshalUserLastContentLastContentLeafContent(v *UserLastContentLastContentLeafContent) ([]byte, error) {
+
+	var typename string
+	switch v := (*v).(type) {
+	case *UserLastContentLastContentArticle:
+		typename = "Article"
+
+		premarshaled, err := v.__premarshalJSON()
+		if err != nil {
+			return nil, err
+		}
+		result := struct {
+			TypeName string `json:"__typename"`
+			*__premarshalUserLastContentLastContentArticle
+		}{typename, premarshaled}
+		return json.Marshal(result)
+	case *UserLastContentLastContentVideo:
+		typename = "Video"
+
+		premarshaled, err := v.__premarshalJSON()
+		if err != nil {
+			return nil, err
+		}
+		result := struct {
+			TypeName string `json:"__typename"`
+			*__premarshalUserLastContentLastContentVideo
+		}{typename, premarshaled}
+		return json.Marshal(result)
+	case nil:
+		return []byte("null"), nil
+	default:
+		return nil, fmt.Errorf(
+			`unexpected concrete type for UserLastContentLastContentLeafContent: "%T"`, v)
+	}
+}
+
+// UserLastContentLastContentVideo includes the requested fields of the GraphQL type Video.
+type UserLastContentLastContentVideo struct {
+	Typename               string `json:"__typename"`
+	SimpleLeafContentVideo `json:"-"`
+}
+
+// GetTypename returns UserLastContentLastContentVideo.Typename, and is useful for accessing the field via an interface.
+func (v *UserLastContentLastContentVideo) GetTypename() string { return v.Typename }
+
+// GetId returns UserLastContentLastContentVideo.Id, and is useful for accessing the field via an interface.
+func (v *UserLastContentLastContentVideo) GetId() testutil.ID { return v.SimpleLeafContentVideo.Id }
+
+func (v *UserLastContentLastContentVideo) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*UserLastContentLastContentVideo
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.UserLastContentLastContentVideo = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(
+		b, &v.SimpleLeafContentVideo)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+type __premarshalUserLastContentLastContentVideo struct {
+	Typename string `json:"__typename"`
+
+	Id testutil.ID `json:"id"`
+}
+
+func (v *UserLastContentLastContentVideo) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *UserLastContentLastContentVideo) __premarshalJSON() (*__premarshalUserLastContentLastContentVideo, error) {
+	var retval __premarshalUserLastContentLastContentVideo
+
+	retval.Typename = v.Typename
+	retval.Id = v.SimpleLeafContentVideo.Id
+	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"`
+	Name               string               `json:"name"`
+	Url                string               `json:"url"`
+	Duration           int                  `json:"duration"`
+	Thumbnail          VideoFieldsThumbnail `json:"thumbnail"`
+	ContentFieldsVideo `json:"-"`
+}
+
+// GetId returns VideoFields.Id, and is useful for accessing the field via an interface.
+func (v *VideoFields) GetId() testutil.ID { return v.Id }
+
+// GetName returns VideoFields.Name, and is useful for accessing the field via an interface.
+func (v *VideoFields) GetName() string { return v.Name }
+
+// GetUrl returns VideoFields.Url, and is useful for accessing the field via an interface.
+func (v *VideoFields) GetUrl() string { return v.Url }
+
+// GetDuration returns VideoFields.Duration, and is useful for accessing the field via an interface.
+func (v *VideoFields) GetDuration() int { return v.Duration }
+
+// GetThumbnail returns VideoFields.Thumbnail, and is useful for accessing the field via an interface.
+func (v *VideoFields) GetThumbnail() VideoFieldsThumbnail { return v.Thumbnail }
+
+func (v *VideoFields) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*VideoFields
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.VideoFields = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(
+		b, &v.ContentFieldsVideo)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+type __premarshalVideoFields struct {
+	Id testutil.ID `json:"id"`
+
+	Name string `json:"name"`
+
+	Url string `json:"url"`
+
+	Duration int `json:"duration"`
+
+	Thumbnail VideoFieldsThumbnail `json:"thumbnail"`
+}
+
+func (v *VideoFields) MarshalJSON() ([]byte, error) {
+	premarshaled, err := v.__premarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+	return json.Marshal(premarshaled)
+}
+
+func (v *VideoFields) __premarshalJSON() (*__premarshalVideoFields, error) {
+	var retval __premarshalVideoFields
+
+	retval.Id = v.Id
+	retval.Name = v.Name
+	retval.Url = v.Url
+	retval.Duration = v.Duration
+	retval.Thumbnail = v.Thumbnail
+	return &retval, nil
+}
+
+// VideoFieldsThumbnail includes the requested fields of the GraphQL type Thumbnail.
+type VideoFieldsThumbnail struct {
+	Id testutil.ID `json:"id"`
+}
+
+// GetId returns VideoFieldsThumbnail.Id, and is useful for accessing the field via an interface.
+func (v *VideoFieldsThumbnail) GetId() testutil.ID { return v.Id }
+
+// The query or mutation executed by ComplexNamedFragments.
+const ComplexNamedFragments_Operation = `
+query ComplexNamedFragments {
+	... on Query {
+		... QueryFragment
+	}
+}
+fragment QueryFragment on Query {
+	... InnerQueryFragment
+}
+fragment InnerQueryFragment on Query {
+	randomItem {
+		__typename
+		id
+		name
+		... VideoFields
+		... ContentFields
+	}
+	randomLeaf {
+		__typename
+		... VideoFields
+		... MoreVideoFields
+		... ContentFields
+	}
+	otherLeaf: randomLeaf {
+		__typename
+		... on Video {
+			... MoreVideoFields
+			... ContentFields
+		}
+		... ContentFields
 	}
 }
 fragment VideoFields on Video {
@@ -1821,3 +2620,56 @@ func ComplexNamedFragments(
 	return &data_, err_
 }
 
+// The query or mutation executed by ComplexNamedFragmentsWithInlineUnion.
+const ComplexNamedFragmentsWithInlineUnion_Operation = `
+query ComplexNamedFragmentsWithInlineUnion {
+	user {
+		... UserLastContent
+	}
+	root {
+		... TopicNewestContent
+	}
+}
+fragment UserLastContent on User {
+	lastContent {
+		__typename
+		... SimpleLeafContent
+	}
+}
+fragment TopicNewestContent on Topic {
+	newestContent {
+		__typename
+		... SimpleLeafContent
+	}
+}
+fragment SimpleLeafContent on LeafContent {
+	... on Article {
+		id
+	}
+	... on Video {
+		id
+	}
+}
+`
+
+func ComplexNamedFragmentsWithInlineUnion(
+	client_ graphql.Client,
+) (*ComplexNamedFragmentsWithInlineUnionResponse, error) {
+	req_ := &graphql.Request{
+		OpName: "ComplexNamedFragmentsWithInlineUnion",
+		Query:  ComplexNamedFragmentsWithInlineUnion_Operation,
+	}
+	var err_ error
+
+	var data_ ComplexNamedFragmentsWithInlineUnionResponse
+	resp_ := &graphql.Response{Data: &data_}
+
+	err_ = client_.MakeRequest(
+		nil,
+		req_,
+		resp_,
+	)
+
+	return &data_, err_
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json
index 114023c..f78ee92 100644
--- a/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json
+++ b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.json
@@ -4,6 +4,11 @@
       "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\t... ContentFields\n\t}\n\trandomLeaf {\n\t\t__typename\n\t\t... VideoFields\n\t\t... MoreVideoFields\n\t\t... ContentFields\n\t}\n\totherLeaf: randomLeaf {\n\t\t__typename\n\t\t... on Video {\n\t\t\t... MoreVideoFields\n\t\t\t... ContentFields\n\t\t}\n\t\t... ContentFields\n\t}\n}\nfragment VideoFields on Video {\n\tid\n\tname\n\turl\n\tduration\n\tthumbnail {\n\t\tid\n\t}\n\t... ContentFields\n}\nfragment ContentFields on Content {\n\tname\n\turl\n}\nfragment MoreVideoFields on Video {\n\tid\n\tparent {\n\t\tname\n\t\turl\n\t\t... ContentFields\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"
+    },
+    {
+      "operationName": "ComplexNamedFragmentsWithInlineUnion",
+      "query": "\nquery ComplexNamedFragmentsWithInlineUnion {\n\tuser {\n\t\t... UserLastContent\n\t}\n\troot {\n\t\t... TopicNewestContent\n\t}\n}\nfragment UserLastContent on User {\n\tlastContent {\n\t\t__typename\n\t\t... SimpleLeafContent\n\t}\n}\nfragment TopicNewestContent on Topic {\n\tnewestContent {\n\t\t__typename\n\t\t... SimpleLeafContent\n\t}\n}\nfragment SimpleLeafContent on LeafContent {\n\t... on Article {\n\t\tid\n\t}\n\t... on Video {\n\t\tid\n\t}\n}\n",
+      "sourceLocation": "testdata/queries/ComplexNamedFragments.graphql"
     }
   ]
 }