Repository: Khan/genqlient
Pull request #96 (2021-09-15 19:38:22)

Add open-source boilerplate
## Summary:
There's a lot these days!  I got most of it from the GitHub docs [1], as
well as looking at other projects.  I'll probably hold off on landing
this until a couple more things land, then land this and tag v0.1.0.

[1] https://docs.github.com/en/communities

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

## Test plan: view in github UI


benjaminjkraft: See also: https://github.com/Khan/khan.github.io/pull/17
dangoor: Nice! Yeah, there are a lot of little bits and pieces, but it does make for a cleaner launch. Thanks!

Patch:
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..5859e82
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,23 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior. Please include whether the problem happens at code-generation time or at runtime.
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+**genqlient version**
+Include your genqlient version here.
+
+**Additional context**
+Add any other context about the problem here.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..11fc491
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/ISSUE_TEMPLATE/new-issue.md b/.github/ISSUE_TEMPLATE/new-issue.md
deleted file mode 100644
index b35fcf0..0000000
--- a/.github/ISSUE_TEMPLATE/new-issue.md
+++ /dev/null
@@ -1,10 +0,0 @@
----
-name: New issue
-about: gqlgen is not ready for production use!
-title: ''
-labels: ''
-assignees: ''
-
----
-
-genqlient is not ready for production use!  While bug reports and feature requests are welcome, we do not guarantee any response.
diff --git a/.github/ISSUE_TEMPLATE/security.md b/.github/ISSUE_TEMPLATE/security.md
new file mode 100644
index 0000000..7359c6c
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/security.md
@@ -0,0 +1,12 @@
+---
+name: Security
+about: Security issues should be reported on HackerOne.
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**STOP!** Security issues should be reported to the [Khan Academy HackerOne program](https://hackerone.com/khanacademy).
+
+**DO NOT REPORT SECURITY ISSUES ON GITHUB.**
diff --git a/.golangci.yml b/.golangci.yml
index 4a7835d..a2de0fa 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -30,6 +30,7 @@ linters:
     - unconvert
     - unparam
     - whitespace
+    - forbidigo
 
 linters-settings:
   errcheck:
@@ -52,6 +53,12 @@ linters-settings:
       - gopkg.in/yaml.v2
       - github.com/alexflint/go-arg
 
+  forbidigo:
+    forbid:
+      - '^print(|f|ln)$'
+      - '^fmt\.Print(|f|ln)$'
+
+
   gocritic:
     # Which checks should be enabled:
     # See https://go-critic.github.io/overview#checks-overview
@@ -82,7 +89,12 @@ issues:
     # Test-only deps are not restricted.
     - linters:
         - depguard
-      path: _test\.go$|internal/testutil/|internal/integration/
+      path: _test\.go$|^internal/testutil/|^internal/integration/
+
+    # Ok to use fmt.Print in the examples, and in the CLI entrypoint.
+    - linters:
+        - forbidigo
+      path: ^example/|^generate/main\.go$
 
     - linters:
         - errcheck
diff --git a/Makefile b/Makefile
index becdf4e..c111727 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ example:
 
 lint:
 	( cd internal/lint && go build -o golangci-lint github.com/golangci/golangci-lint/cmd/golangci-lint )
-	internal/lint/golangci-lint run ./...
+	internal/lint/golangci-lint run ./... --fix
 
 check: lint
 	go test -cover ./...
diff --git a/README.md b/README.md
index 8926093..cf6be49 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,10 @@
 <img width="100%" alt="generated graphql client ⇒ genqlient" src="docs/images/genqlient.svg">
 
+[![Go Reference](https://pkg.go.dev/badge/github.com/Khan/genqlient.svg)](https://pkg.go.dev/github.com/Khan/genqlient)
+[![Test Status](https://github.com/Khan/genqlient/actions/workflows/go.yml/badge.svg)](https://github.com/Khan/genqlient/actions)
+[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](docs/CODE_OF_CONDUCT.md)
+[![GoReportcard](https://goreportcard.com/badge/github.com/Khan/genqlient?status.svg)](https://goreportcard.com/report/github.com/Khan/genqlient)
+
 # genqlient: a truly type-safe Go GraphQL client
 
 This is a proof-of-concept of using code-generation to create a truly type-safe GraphQL client in Go.  It is not yet ready for contributions (see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md)).
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
new file mode 100644
index 0000000..19ddb43
--- /dev/null
+++ b/docs/CHANGELOG.md
@@ -0,0 +1,31 @@
+# Changelog
+
+<!--
+
+When releasing a new version:
+- change "next" to the new version, and delete any empty sections
+- copy the below template to be the new "next"
+
+## template
+
+### Breaking changes:
+
+### New features:
+
+### Bug fixes:
+
+-->
+
+## next
+
+<!-- Add new changes in this section! -->
+
+### Breaking changes:
+
+### New features:
+
+### Bug fixes:
+
+## v0.1.0
+
+First open-sourced version.
diff --git a/docs/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..94c64fe
--- /dev/null
+++ b/docs/CODE_OF_CONDUCT.md
@@ -0,0 +1,132 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, caste, color, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+  and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+  overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+  advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+  address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+opensource[at]khanacademy[dot]org.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior,  harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.1, available at
+[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
+
+Community Impact Guidelines were inspired by
+[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
+
+For answers to common questions about this code of conduct, see the FAQ at
+[https://www.contributor-covenant.org/faq][FAQ]. Translations are available
+at [https://www.contributor-covenant.org/translations][translations].
+
+[homepage]: https://www.contributor-covenant.org
+[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
+[Mozilla CoC]: https://github.com/mozilla/diversity
+[FAQ]: https://www.contributor-covenant.org/faq
+[translations]: https://www.contributor-covenant.org/translations
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 06b76ab..ab92499 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -1,8 +1,34 @@
 # Contributing to genqlient
 
-genqlient is not yet accepting contributions.  The library is still in a very rough state, so while we intend to accept contributions in the future, we're not ready for them just yet.  Please contact us (via an issue or email) if you are interested in helping out, but please be aware the answer may be that we aren't ready for your help yet, even if such help will be greatly useful once we are!  We'll be ready for the rest of the world soon.
+genqlient welcomes contributions from the community!  Our [help wanted](https://github.com/Khan/genqlient/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) label tags issues where community help is especially welcome, and [good first issue](https://github.com/Khan/genqlient/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) lists issues that are likely most approachable for new contributors.
 
-Khan Academy is a non-profit organization with a mission to provide a free, world-class education to anyone, anywhere. If you're looking for other ways to help us, You can help us in that mission by [donating](https://khanacademy.org/donate) or looking at [career opportunities](https://khanacademy.org/careers).
+While we're on the subject, Khan Academy is a non-profit organization with a mission to provide a free, world-class education to anyone, anywhere. If you're looking for other ways to help us, you can help us in that mission by [donating](https://khanacademy.org/donate) or looking at [career opportunities](https://khanacademy.org/careers).
+
+## Formalities
+
+In order to contribute to genqlient, you must first sign the [Khan CLA](https://www.khanacademy.org/r/cla). genqlient is released under the [MIT License](../LICENSE). While contributing, you must abide by the [Code of Conduct](CODE_OF_CONDUCT.md).
+
+## Security issues
+
+Security issues should be reported to the [Khan Academy HackerOne program](https://hackerone.com/khanacademy).
+
+## Pull requests
+
+Pull requests should have:
+
+- a clear PR title and description, describing the changes
+- a link to the issue fixed, if applicable
+- test coverage, as appropriate (see [below](#tests))
+- documentation, for new features
+- changelog entries
+
+Pull requests will be squash-merged, so subsequent commit messages may be brief (e.g. "review comments").
+
+Large changes should typically be discussed on the issue tracker first, and should ideally be broken up into separate PRs, or failing that, several commits, for ease of reviewing.
+
+## Style
+
+Go style should generally follow the conventions of [Effective Go](https://golang.org/doc/effective_go), and should have no lint errors (`make lint` to check). TODOs follow the [Khan Academy style](https://github.com/Khan/style-guides#todosmessage), i.e. `// TODO(yourusername): Simplify once we drop support for Go 1.23.` In general, try to make your code match the style of the surrounding code.
 
 ## Tests
 
@@ -12,7 +38,8 @@ Notes for contributors:
 - Most of the tests are snapshot-based; see `generate/generate_test.go`.  All new code-generation logic should be snapshot-tested.  Some code additionally has standalone unit tests, when convenient.
 - Integration tests run against a gqlgen server in `internal/integration/integration_test.go`, and should cover everything that snapshot tests can't, including the GraphQL client code and JSON marshaling.
 - If `GITHUB_TOKEN` is available in the environment, it also checks that the example returns the expected output when run against the real API.  This is configured automatically in GitHub Actions, but you can also use a [personal access token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) with no scopes.  There's no need for this to cover anything in particular; it's just to make sure the example in fact works.
+- Tests should use `testify/assert` and `testify/require` where convenient (when making many simple assertions).
 
-## Design
+## Finding your way around
 
-See [DESIGN.md](DESIGN.md) for documentation of major design decisions in this library.
+If you're new to genqlient, start out by reading the source of `generate.Generate`, whose comments describe most of the high-level operation of genqlient.  In general, the code is documented inline, often with an introductory comment at the top of the file.  See [DESIGN.md](DESIGN.md) for documentation of major design decisions, which is a good way to get a sense of why genqlient is structured the way it is.
diff --git a/docs/FAQ.md b/docs/FAQ.md
index 0e54a34..b918132 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -66,6 +66,18 @@ if errors.As(err, &errList) {
 }
 ```
 
+### … use custom scalars?
+
+Just tell genqlient via the `bindings` option in `genqlient.yaml`:
+
+```yaml
+bindings:
+  DateTime:
+    type: time.Time
+```
+
+Make sure the given type has whatever logic is needed to convert to/from JSON (e.g. `MarshalJSON`/`UnmarshalJSON` or JSON tags).  See the [`genqlient.yaml` documentation](genqlient.yaml) for the full syntax.
+
 ### … require 32-bit integers?
 
 The GraphQL spec officially defines the `Int` type to be a [signed 32-bit integer](https://spec.graphql.org/draft/#sec-Int).  GraphQL clients and servers vary wildly in their enforcement of this; for example:
@@ -183,6 +195,151 @@ if novel, ok := resp.Favorite.(*GetBooksFavoriteNovel); ok {
 
 The interface-type's GoDoc will include a list of its implementations, for your convenience.
 
+If you only want to request shared fields of the interface (i.e. no fragments), this may seem like a lot of ceremony.  If you prefer, you can instead add `# @genqlient(struct: true)` to the field, and genqlient will just generate a struct, like it does for GraphQL object types.  For example, given:
+
+```graphql
+query GetBooks {
+  # @genqlient(struct: true)
+  favorite {
+    title
+  }
+}
+```
+
+genqlient will generate just:
+
+```go
+type GetBooksFavoriteBook struct {
+  Title string
+}
+```
+
+Keep in mind that if you later want to add fragments to your selection, you won't be able to use `struct` anymore; when you remove it you may need to update your code to replace `.Title` with `.GetTitle()` and so on.
+
+
+### … shared types between different parts of the query?
+
+Suppose you have a query which requests several different fields each of the same GraphQL type, e.g. `User` (or `[User]`):
+
+```graphql
+query GetMonopolyPlayers {
+  game {
+    winner { id name }
+    banker { id name }
+    spectators { id name }
+  }
+}
+```
+
+This will produce a Go type like:
+```go
+type GetMonopolyPlayersGame struct {
+  Winner     GetMonopolyPlayersGameWinnerUser
+  Banker     GetMonopolyPlayersGameBankerUser
+  Spectators []GetMonopolyPlayersGameSpectatorsUser
+}
+
+type GetMonopolyPlayersGameWinnerUser struct {
+  Id   string
+  Name string
+}
+
+// (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.
+
+One option -- the GraphQL Way, perhaps -- is to use fragments.  You'd write your query like:
+
+```graphql
+fragment MonopolyUser on User {
+  id
+  name
+}
+
+query GetMonopolyPlayers {
+  game {
+    winner { ...MonopolyUser }
+    banker { ...MonopolyUser }
+    spectators { ...MonopolyUser }
+  }
+}
+```
+
+genqlient will notice this, and generate a type corresponding to the fragment; `GetMonopolyPlayersGame` will look as before, but each of the field types will have a shared embed:
+
+```go
+type MonopolyUser struct {
+  Id   string
+  Name string
+}
+
+type GetMonopolyPlayersGameWinnerUser struct {
+  MonopolyUser
+}
+
+// (others similarly)
+```
+
+Thus you can have `FormatUser` accept a `MonopolyUser`, and pass it `game.Winner.MonopolyUser`, `game.Spectators[i].MonopolyUser`, etc.  This is convenient if you may later want to add other fields to some of the queries, because you can still do
+
+```graphql
+fragment MonopolyUser on User {
+  id
+  name
+}
+
+query GetMonopolyPlayers {
+  game {
+    winner {
+      winCount
+      ...MonopolyUser
+    }
+    banker {
+      bankerRating
+      ...MonopolyUser
+    }
+    spectators { ...MonopolyUser }
+  }
+}
+```
+
+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`:
+
+```graphql
+query GetMonopolyPlayers {
+  game {
+    # @genqlient(typename: "User")
+    winner { id name }
+    # @genqlient(typename: "User")
+    banker { id name }
+    # @genqlient(typename: "User")
+    spectators { id name }
+  }
+}
+```
+
+This will tell genqlient to use the same types for each field:
+
+```go
+type GetMonopolyPlayersGame struct {
+  Winner     User
+  Banker     User
+  Spectators []User
+}
+
+type User struct {
+  Id   string
+  Name string
+}
+```
+
+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.
+
 ### … documentation on the output types?
 
 For any GraphQL types or fields with documentation in the GraphQL schema, genqlient automatically includes that documentation in the generated code's GoDoc.  To add additional information to genqlient entrypoints, you can put comments in the GraphQL source:
@@ -236,6 +393,21 @@ type User = GetFamilyNamesUser
 type ChildUser = GetFamilyNamesUserChildrenUser
 ```
 
+Alternately, you can use the `typename` option: if you query
+```graphql
+query GetFamilyNames {
+  # @genqlient(typename: "User")
+  user {
+    name
+    # @genqlient(typename: "ChildUser")
+    children {
+      name
+    }
+  }
+}
+```
+genqlient will instead generate types with the given names.  (You'll need to avoid conflicts; see the [full documentation](genqlient_directive.graphql) for details.)
+
 ### … my editor/IDE plugin not know about the code genqlient just generated?
 
 If your tools are backed by [gopls](https://github.com/golang/tools/blob/master/gopls/README.md) (which is most of them), they simply don't know it was updated.  In most cases, keeping the generated file (typically `generated.go`) open in the background, and reloading it after each run of `genqlient`, will do the trick.
diff --git a/docs/SECURITY.md b/docs/SECURITY.md
new file mode 100644
index 0000000..264d3cb
--- /dev/null
+++ b/docs/SECURITY.md
@@ -0,0 +1,3 @@
+# Security policy
+
+Security issues should be reported to the [Khan Academy HackerOne program](https://hackerone.com/khanacademy).
diff --git a/docs/genqlient_directive.graphql b/docs/genqlient_directive.graphql
index 328d8b8..5a845c1 100644
--- a/docs/genqlient_directive.graphql
+++ b/docs/genqlient_directive.graphql
@@ -54,6 +54,22 @@ directive genqlient(
   # zero value and null (for nullable fields).
   pointer: Boolean
 
+  # If set, this field will use a struct type in Go, even if it's an interface.
+  #
+  # This is useful when you have a query like
+  #  query MyQuery {
+  #    myInterface { myField }
+  #  }
+  # where you are requesting only shared fields of an interface.  By default,
+  # genqlient still generates an interface type, for consistency.  But this
+  # isn't necessary: a struct would do just fine since there are no
+  # type-specific fields.  Setting `struct: true` tells genqlient to do that.
+  #
+  # Note that this is only allowed when there are no fragments in play, such
+  # that all fields are on the interface type.  Note that if you later add a
+  # fragment, you'll have to remove this option, and the types will change.
+  struct: Boolean
+
   # If set, this argument or field will use the given Go type instead of a
   # genqlient-generated type.
   #
@@ -75,6 +91,38 @@ directive genqlient(
   # genqlient-generated type.
   bind: String
 
+  # If set, the type of this field will have the given name in Go.
+  #
+  # For example, given the following query:
+  #  # @genqlient(typename: "MyResp")
+  #  query MyQuery {
+  #    # @genqlient(typename: "User")
+  #    user {
+  #      id
+  #    }
+  #  }
+  # genqlient will generate
+  #  type Resp struct {
+  #    User User
+  #  }
+  #  type User struct {
+  #    Id string
+  #  }
+  # instead of its usual, more verbose type names.
+  #
+  # With great power comes great responsibility: when using typename you'll
+  # need to avoid comments; genqlient will complain if you use the same
+  # type-name in multiple places unless they request the exact same fields, or
+  # 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).
+  #
+  # Note that unlike most directives, if applied to the entire operation,
+  # typename affects the overall response type, rather than being propagated
+  # down to all child fields (which would cause conflicts).
+  typename: String
+
 ) on
   # genqlient directives can go almost anywhere, although some options are only
   # applicable in certain locations as described above.
diff --git a/generate/convert.go b/generate/convert.go
index 3528746..609c993 100644
--- a/generate/convert.go
+++ b/generate/convert.go
@@ -15,6 +15,61 @@ import (
 	"github.com/vektah/gqlparser/v2/ast"
 )
 
+// getType returns the existing type in g.typeMap with the given name, if any,
+// and an error if such type is incompatible with this one.
+//
+// This is useful as an early-out and a safety-check when generating types; if
+// the type has already been generated we can skip generating it again.  (This
+// is necessary to handle recursive input types, and an optimization in other
+// cases.)
+func (g *generator) getType(
+	goName, graphQLName string,
+	selectionSet ast.SelectionSet,
+	pos *ast.Position,
+) (goType, error) {
+	typ, ok := g.typeMap[goName]
+	if !ok {
+		return nil, nil
+	}
+
+	if typ.GraphQLTypeName() != graphQLName {
+		return typ, errorf(
+			pos, "conflicting definition for %s; this can indicate either "+
+				"a genqlient internal error, a conflict between user-specified "+
+				"type-names, or some very tricksy GraphQL field/type names: "+
+				"expected GraphQL type %s, got %s",
+			goName, typ.GraphQLTypeName(), graphQLName)
+	}
+
+	expectedSelectionSet := typ.SelectionSet()
+	if err := selectionsMatch(pos, selectionSet, expectedSelectionSet); err != nil {
+		return typ, errorf(
+			pos, "conflicting definition for %s; this can indicate either "+
+				"a genqlient internal error, a conflict between user-specified "+
+				"type-names, or some very tricksy GraphQL field/type names: %v",
+			goName, err)
+	}
+
+	return typ, nil
+}
+
+// addType inserts the type into g.typeMap, checking for conflicts.
+//
+// The conflict-checking is as described in getType.  Note we have to do it
+// here again, even if the caller has already called getType, because the
+// caller in between may have generated new types, which potentially creates
+// new conflicts.
+//
+// Returns an already-existing type if found, and otherwise the given type.
+func (g *generator) addType(typ goType, goName string, pos *ast.Position) (goType, error) {
+	otherTyp, err := g.getType(goName, typ.GraphQLTypeName(), typ.SelectionSet(), pos)
+	if otherTyp != nil || err != nil {
+		return otherTyp, err
+	}
+	g.typeMap[goName] = typ
+	return typ, nil
+}
+
 // baseTypeForOperation returns the definition of the GraphQL type to which the
 // root of the operation corresponds, e.g. the "Query" or "Mutation" type.
 func (g *generator) baseTypeForOperation(operation ast.Operation) (*ast.Definition, error) {
@@ -40,9 +95,10 @@ func (g *generator) convertOperation(
 	queryOptions *genqlientDirective,
 ) (goType, error) {
 	name := operation.Name + "Response"
-
-	if def, ok := g.typeMap[name]; ok {
-		return nil, errorf(operation.Position, "%s defined twice:\n%s", name, def)
+	namePrefix := newPrefixList(operation.Name)
+	if queryOptions.TypeName != "" {
+		name = queryOptions.TypeName
+		namePrefix = newPrefixList(queryOptions.TypeName)
 	}
 
 	baseType, err := g.baseTypeForOperation(operation.Operation)
@@ -54,7 +110,7 @@ func (g *generator) convertOperation(
 	// thing, because we want to do a few things differently, and because we
 	// know we have an object type, so we can include only that case.
 	fields, err := g.convertSelectionSet(
-		newPrefixList(operation.Name), operation.SelectionSet, baseType, queryOptions)
+		namePrefix, operation.SelectionSet, baseType, queryOptions)
 	if err != nil {
 		return nil, err
 	}
@@ -67,11 +123,11 @@ func (g *generator) convertOperation(
 			GraphQLName: baseType.Name,
 			// omit the GraphQL description for baseType; it's uninteresting.
 		},
-		Fields: fields,
+		Fields:    fields,
+		Selection: operation.SelectionSet,
 	}
-	g.typeMap[name] = goType
 
-	return goType, nil
+	return g.addType(goType, goType.GoName, operation.Position)
 }
 
 var builtinTypes = map[string]string{
@@ -110,7 +166,7 @@ func (g *generator) convertType(
 	localBinding := options.Bind
 	if localBinding != "" && localBinding != "-" {
 		goRef, err := g.addRef(localBinding)
-		return &goOpaqueType{goRef}, err
+		return &goOpaqueType{goRef, typ.Name()}, err
 	}
 
 	if typ.Elem != nil {
@@ -162,11 +218,46 @@ func (g *generator) convertDefinition(
 			}
 		}
 		goRef, err := g.addRef(globalBinding.Type)
-		return &goOpaqueType{goRef}, err
+		return &goOpaqueType{goRef, def.Name}, err
 	}
 	goBuiltinName, ok := builtinTypes[def.Name]
 	if ok {
-		return &goOpaqueType{goBuiltinName}, nil
+		return &goOpaqueType{goBuiltinName, def.Name}, nil
+	}
+
+	// Determine the name to use for this type.
+	var name string
+	if options.TypeName != "" {
+		// If the user specified a name, use it!
+		name = options.TypeName
+		if namePrefix.head == name && namePrefix.tail == nil {
+			// Special case: if this name is also the only component of the
+			// name-prefix, append the type-name anyway.  This happens when you
+			// assign a type name to an interface type, and we are generating
+			// one of its implementations.
+			name = makeLongTypeName(namePrefix, def.Name)
+		}
+		// (But the prefix is shared.)
+		namePrefix = newPrefixList(options.TypeName)
+	} else if def.Kind == ast.InputObject || def.Kind == ast.Enum {
+		// If we're an input-object or enum, there is only one type we will
+		// ever possibly generate for this type, so we don't need any of the
+		// qualifiers.  This is especially helpful because the caller is very
+		// likely to need to reference these types in their code.
+		name = upperFirst(def.Name)
+		// (namePrefix is ignored in this case.)
+	} else {
+		// Else, construct a name using the usual algorithm (see names.go).
+		name = makeTypeName(namePrefix, def.Name)
+	}
+
+	// If we already generated the type, we can skip it as long as it matches
+	// (and must fail if it doesn't).  (This can happen for input/enum types,
+	// types of fields of interfaces, when options.TypeName is set, or, of
+	// course, on invalid configuration or internal error.)
+	existing, err := g.getType(name, def.Name, selectionSet, pos)
+	if existing != nil || err != nil {
+		return existing, err
 	}
 
 	desc := descriptionInfo{
@@ -175,10 +266,15 @@ func (g *generator) convertDefinition(
 		GraphQLName:        def.Name,
 	}
 
-	switch def.Kind {
+	// The struct option basically means "treat this as if it were an object".
+	// (It only applies if valid; this is important if you said the whole
+	// query should have `struct: true`.)
+	kind := def.Kind
+	if options.GetStruct() && validateStructOption(def, selectionSet, pos) == nil {
+		kind = ast.Object
+	}
+	switch kind {
 	case ast.Object:
-		name := makeTypeName(namePrefix, def.Name)
-
 		fields, err := g.convertSelectionSet(
 			namePrefix, selectionSet, def, queryOptions)
 		if err != nil {
@@ -188,25 +284,24 @@ func (g *generator) convertDefinition(
 		goType := &goStructType{
 			GoName:          name,
 			Fields:          fields,
+			Selection:       selectionSet,
 			descriptionInfo: desc,
 		}
-		g.typeMap[name] = goType
-		return goType, nil
+		return g.addType(goType, goType.GoName, pos)
 
 	case ast.InputObject:
-		// If we're an input-object, there is only one type we will ever
-		// possibly generate for this type, so we don't need any of the
-		// qualifiers.  This is especially helpful because the caller is very
-		// likely to need to reference these types in their code.
-		name := upperFirst(def.Name)
-
 		goType := &goStructType{
 			GoName:          name,
 			Fields:          make([]*goStructField, len(def.Fields)),
 			descriptionInfo: desc,
 			IsInput:         true,
 		}
-		g.typeMap[name] = goType
+		// To handle recursive types, we need to add the type to the type-map
+		// *before* converting its fields.
+		_, err := g.addType(goType, goType.GoName, pos)
+		if err != nil {
+			return nil, err
+		}
 
 		for i, field := range def.Fields {
 			goName := upperFirst(field.Name)
@@ -235,8 +330,6 @@ func (g *generator) convertDefinition(
 		return goType, nil
 
 	case ast.Interface, ast.Union:
-		name := makeTypeName(namePrefix, def.Name)
-
 		sharedFields, err := g.convertSelectionSet(
 			namePrefix, selectionSet, def, queryOptions)
 		if err != nil {
@@ -248,9 +341,9 @@ func (g *generator) convertDefinition(
 			GoName:          name,
 			SharedFields:    sharedFields,
 			Implementations: make([]*goStructType, len(implementationTypes)),
+			Selection:       selectionSet,
 			descriptionInfo: desc,
 		}
-		g.typeMap[name] = goType
 
 		for i, implDef := range implementationTypes {
 			// TODO(benkraft): In principle we should skip generating a Go
@@ -272,24 +365,19 @@ func (g *generator) convertDefinition(
 			}
 			goType.Implementations[i] = implStructTyp
 		}
-		return goType, nil
+		return g.addType(goType, goType.GoName, pos)
 
 	case ast.Enum:
-		// Like with InputObject, there's only one type we will ever generate
-		// for an enum.
-		name := upperFirst(def.Name)
-
 		goType := &goEnumType{
 			GoName:      name,
+			GraphQLName: def.Name,
 			Description: def.Description,
 			Values:      make([]goEnumValue, len(def.EnumValues)),
 		}
-		g.typeMap[name] = goType
-
 		for i, val := range def.EnumValues {
 			goType.Values[i] = goEnumValue{Name: val.Name, Description: val.Description}
 		}
-		return goType, nil
+		return g.addType(goType, goType.GoName, pos)
 
 	case ast.Scalar:
 		// (If you had an entry in bindings, we would have returned it above.)
@@ -463,7 +551,7 @@ func (g *generator) convertInlineFragment(
 	containingTypedef *ast.Definition,
 	queryOptions *genqlientDirective,
 ) ([]*goStructField, error) {
-	// You might think fragmentTypedef would be a fragment.ObjectDefinition, but
+	// You might think fragmentTypedef is just fragment.ObjectDefinition, but
 	// actually that's the type into which the fragment is spread.
 	fragmentTypedef := g.schema.Types[fragment.TypeCondition]
 	if !fragmentMatches(containingTypedef, fragmentTypedef) {
@@ -552,6 +640,7 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy
 		goType := &goStructType{
 			GoName:          fragment.Name,
 			Fields:          fields,
+			Selection:       fragment.SelectionSet,
 			descriptionInfo: desc,
 		}
 		g.typeMap[fragment.Name] = goType
@@ -562,6 +651,7 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy
 			GoName:          fragment.Name,
 			SharedFields:    fields,
 			Implementations: make([]*goStructType, len(implementationTypes)),
+			Selection:       fragment.SelectionSet,
 			descriptionInfo: desc,
 		}
 		g.typeMap[fragment.Name] = goType
@@ -579,6 +669,7 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy
 			implTyp := &goStructType{
 				GoName:          fragment.Name + upperFirst(implDef.Name),
 				Fields:          implFields,
+				Selection:       fragment.SelectionSet,
 				descriptionInfo: implDesc,
 			}
 			goType.Implementations[i] = implTyp
diff --git a/generate/generate.go b/generate/generate.go
index 7c6614f..2adbf3c 100644
--- a/generate/generate.go
+++ b/generate/generate.go
@@ -209,6 +209,8 @@ func (g *generator) preprocessQueryDocument(doc *ast.QueryDocument) {
 	// 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.
+	// TODO(benkraft): We should omit __typename if you asked for
+	// `# @genqlient(struct: true)`.
 	observers.OnField(func(_ *validator.Walker, field *ast.Field) {
 		// We are interested in a field from the query like
 		//	field { subField ... }
diff --git a/generate/genqlient_directive.go b/generate/genqlient_directive.go
index 43b8ce4..ca00aad 100644
--- a/generate/genqlient_directive.go
+++ b/generate/genqlient_directive.go
@@ -14,11 +14,14 @@ type genqlientDirective struct {
 	pos       *ast.Position
 	Omitempty *bool
 	Pointer   *bool
+	Struct    *bool
 	Bind      string
+	TypeName  string
 }
 
 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 setBool(dst **bool, v *ast.Value) error {
 	ei, err := v.Value(nil) // no vars allowed
@@ -44,15 +47,15 @@ func setString(dst *string, v *ast.Value) error {
 	return errorf(v.Position, "expected string, got non-string value %T(%v)", ei, ei)
 }
 
-func fromGraphQL(dir *ast.Directive) (*genqlientDirective, error) {
+func fromGraphQL(dir *ast.Directive, pos *ast.Position) (*genqlientDirective, error) {
 	if dir.Name != "genqlient" {
 		// Actually we just won't get here; we only get here if the line starts
 		// with "# @genqlient", unless there's some sort of bug.
-		return nil, errorf(dir.Position, "the only valid comment-directive is @genqlient, got %v", dir.Name)
+		return nil, errorf(pos, "the only valid comment-directive is @genqlient, got %v", dir.Name)
 	}
 
 	var retval genqlientDirective
-	retval.pos = dir.Position
+	retval.pos = pos
 
 	var err error
 	for _, arg := range dir.Arguments {
@@ -62,10 +65,14 @@ func fromGraphQL(dir *ast.Directive) (*genqlientDirective, error) {
 			err = setBool(&retval.Omitempty, arg.Value)
 		case "pointer":
 			err = setBool(&retval.Pointer, arg.Value)
+		case "struct":
+			err = setBool(&retval.Struct, arg.Value)
 		case "bind":
 			err = setString(&retval.Bind, arg.Value)
+		case "typename":
+			err = setString(&retval.TypeName, arg.Value)
 		default:
-			return nil, errorf(arg.Position, "unknown argument %v for @genqlient", arg.Name)
+			return nil, errorf(pos, "unknown argument %v for @genqlient", arg.Name)
 		}
 		if err != nil {
 			return nil, err
@@ -74,7 +81,7 @@ func fromGraphQL(dir *ast.Directive) (*genqlientDirective, error) {
 	return &retval, nil
 }
 
-func (dir *genqlientDirective) validate(node interface{}) error {
+func (dir *genqlientDirective) validate(node interface{}, schema *ast.Schema) error {
 	switch node := node.(type) {
 	case *ast.OperationDefinition:
 		if dir.Bind != "" {
@@ -90,6 +97,10 @@ func (dir *genqlientDirective) validate(node interface{}) error {
 			return errorf(dir.pos, "bind is not implemented for named fragments")
 		}
 
+		if dir.Struct != nil {
+			return errorf(dir.pos, "struct is only applicable to fields")
+		}
+
 		// Like operations, anything else will just apply to the entire
 		// fragment.
 		return nil
@@ -97,17 +108,62 @@ func (dir *genqlientDirective) validate(node interface{}) error {
 		if dir.Omitempty != nil && node.Type.NonNull {
 			return errorf(dir.pos, "omitempty may only be used on optional arguments")
 		}
+
+		if dir.Struct != nil {
+			return errorf(dir.pos, "struct is only applicable to fields")
+		}
+
 		return nil
 	case *ast.Field:
 		if dir.Omitempty != nil {
 			return errorf(dir.pos, "omitempty is not applicable to fields")
 		}
+
+		if dir.Struct != nil {
+			typ := schema.Types[node.Definition.Type.Name()]
+			if err := validateStructOption(typ, node.SelectionSet, dir.pos); err != nil {
+				return err
+			}
+		}
+
 		return nil
 	default:
 		return errorf(dir.pos, "invalid @genqlient directive location: %T", node)
 	}
 }
 
+func validateStructOption(
+	typ *ast.Definition,
+	selectionSet ast.SelectionSet,
+	pos *ast.Position,
+) error {
+	if typ.Kind != ast.Interface && typ.Kind != ast.Union {
+		return errorf(pos, "struct is only applicable to interface-typed fields")
+	}
+
+	// Make sure that all the requested fields apply to the interface itself
+	// (not just certain implementations).
+	for _, selection := range selectionSet {
+		switch selection.(type) {
+		case *ast.Field:
+			// fields are fine.
+		case *ast.InlineFragment, *ast.FragmentSpread:
+			// Fragments aren't allowed. In principle we could allow them under
+			// the condition that the fragment applies to the whole interface
+			// (not just one implementation; and so on recursively), and for
+			// fragment spreads additionally that the fragment has the same
+			// option applied to it, but it seems more trouble than it's worth
+			// right now.
+			return errorf(pos, "struct is not allowed for types with fragments")
+		}
+	}
+	return 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
+// general we take the node's option, then the operation's, then the default).
 func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirective {
 	retval := *dir
 	if other.Omitempty != nil {
@@ -116,9 +172,16 @@ func (dir *genqlientDirective) merge(other *genqlientDirective) *genqlientDirect
 	if other.Pointer != nil {
 		retval.Pointer = other.Pointer
 	}
+	if other.Struct != nil {
+		retval.Struct = other.Struct
+	}
 	if other.Bind != "" {
 		retval.Bind = other.Bind
 	}
+	// For typename, the local directive always wins: when specified on the query
+	// options typename applies to the response-struct, not to all parts of the
+	// query.
+	retval.TypeName = other.TypeName
 	return &retval
 }
 
@@ -141,11 +204,11 @@ func (g *generator) parsePrecedingComment(
 			if err != nil {
 				return "", nil, err
 			}
-			genqlientDirective, err := fromGraphQL(graphQLDirective)
+			genqlientDirective, err := fromGraphQL(graphQLDirective, pos)
 			if err != nil {
 				return "", nil, err
 			}
-			err = genqlientDirective.validate(node)
+			err = genqlientDirective.validate(node, g.schema)
 			if err != nil {
 				return "", nil, err
 			}
diff --git a/generate/names.go b/generate/names.go
index e5173c9..e9bc500 100644
--- a/generate/names.go
+++ b/generate/names.go
@@ -31,7 +31,7 @@ package generate
 // different types.
 //
 // One subtlety in the above description is: is the "MyType" the interface or
-// the impelmentation?  When it's a suffix, the answer is both: we generate
+// the implementation?  When it's a suffix, the answer is both: we generate
 // both MyFieldMyInterface and MyFieldMyImplementation, and the latter, in Go,
 // implements the former.  (See docs/DESIGN.md for more.)  But as an infix, we
 // use the type on which the field is requested.  Concretely, the following
@@ -166,3 +166,13 @@ func nextPrefix(prefix *prefixList, field *ast.Field) *prefixList {
 func makeTypeName(prefix *prefixList, typeName string) string {
 	return joinPrefixList(typeNameParts(prefix, typeName))
 }
+
+// Like makeTypeName, but append typeName unconditionally.
+//
+// This is used for when you specify a type-name for a field of interface
+// type; we use YourName for the interface, but need to do YourNameImplName for
+// the implementations.
+func makeLongTypeName(prefix *prefixList, typeName string) string {
+	typeName = upperFirst(typeName)
+	return joinPrefixList(&prefixList{typeName, prefix})
+}
diff --git a/generate/types.go b/generate/types.go
index d62ee1d..653c0be 100644
--- a/generate/types.go
+++ b/generate/types.go
@@ -9,6 +9,8 @@ import (
 	"fmt"
 	"io"
 	"strings"
+
+	"github.com/vektah/gqlparser/v2/ast"
 )
 
 // goType represents a type for which we'll generate code.
@@ -23,6 +25,16 @@ type goType interface {
 	// used to refer to it in Go code.
 	Reference() string
 
+	// GraphQLTypeName returns the name of the GraphQL type to which this Go type
+	// corresponds.
+	GraphQLTypeName() string
+
+	// SelectionSet returns the selection-set of the GraphQL field from which
+	// this type was generated, or nil if none is applicable (for GraphQL
+	// scalar, enum, and input types, as well as any opaque
+	// (non-genqlient-generated) type since those are validated upon creation).
+	SelectionSet() ast.SelectionSet
+
 	// Remove slice/pointer wrappers, and return the underlying (named (or
 	// builtin)) type.  For example, given []*MyStruct, return MyStruct.
 	Unwrap() goType
@@ -48,7 +60,10 @@ var (
 type (
 	// goOpaqueType represents a user-defined or builtin type, often used to
 	// represent a GraphQL scalar.  (See Config.Bindings for more context.)
-	goOpaqueType struct{ GoRef string }
+	goOpaqueType struct {
+		GoRef       string
+		GraphQLName string
+	}
 	// goSliceType represents the Go type []Elem, used to represent GraphQL
 	// list types.
 	goSliceType struct{ Elem goType }
@@ -67,11 +82,20 @@ func (typ *goOpaqueType) Reference() string  { return typ.GoRef }
 func (typ *goSliceType) Reference() string   { return "[]" + typ.Elem.Reference() }
 func (typ *goPointerType) Reference() string { return "*" + typ.Elem.Reference() }
 
+func (typ *goOpaqueType) SelectionSet() ast.SelectionSet  { return nil }
+func (typ *goSliceType) SelectionSet() ast.SelectionSet   { return typ.Elem.SelectionSet() }
+func (typ *goPointerType) SelectionSet() ast.SelectionSet { return typ.Elem.SelectionSet() }
+
+func (typ *goOpaqueType) GraphQLTypeName() string  { return typ.GraphQLName }
+func (typ *goSliceType) GraphQLTypeName() string   { return typ.Elem.GraphQLTypeName() }
+func (typ *goPointerType) GraphQLTypeName() string { return typ.Elem.GraphQLTypeName() }
+
 // goEnumType represents a Go named-string type used to represent a GraphQL
 // enum.  In this case, we generate both the type (`type T string`) and also a
 // list of consts representing the values.
 type goEnumType struct {
 	GoName      string
+	GraphQLName string
 	Description string
 	Values      []goEnumValue
 }
@@ -96,14 +120,17 @@ func (typ *goEnumType) WriteDefinition(w io.Writer, g *generator) error {
 	return nil
 }
 
-func (typ *goEnumType) Reference() string { return typ.GoName }
+func (typ *goEnumType) Reference() string              { return typ.GoName }
+func (typ *goEnumType) SelectionSet() ast.SelectionSet { return nil }
+func (typ *goEnumType) GraphQLTypeName() string        { return typ.GraphQLName }
 
 // goStructType represents a Go struct type used to represent a GraphQL object
 // or input-object type.
 type goStructType struct {
-	GoName  string
-	Fields  []*goStructField
-	IsInput bool
+	GoName    string
+	Fields    []*goStructField
+	IsInput   bool
+	Selection ast.SelectionSet
 	descriptionInfo
 }
 
@@ -185,7 +212,9 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
 	return g.execute("unmarshal.go.tmpl", w, typ)
 }
 
-func (typ *goStructType) Reference() string { return typ.GoName }
+func (typ *goStructType) Reference() string              { return typ.GoName }
+func (typ *goStructType) SelectionSet() ast.SelectionSet { return typ.Selection }
+func (typ *goStructType) GraphQLTypeName() string        { return typ.GraphQLName }
 
 // goInterfaceType represents a Go interface type, used to represent a GraphQL
 // interface or union type.
@@ -195,6 +224,7 @@ type goInterfaceType struct {
 	// we'll generate getter methods for each.
 	SharedFields    []*goStructField
 	Implementations []*goStructType
+	Selection       ast.SelectionSet
 	descriptionInfo
 }
 
@@ -272,7 +302,9 @@ func (typ *goInterfaceType) WriteDefinition(w io.Writer, g *generator) error {
 	return g.execute("unmarshal_helper.go.tmpl", w, typ)
 }
 
-func (typ *goInterfaceType) Reference() string { return typ.GoName }
+func (typ *goInterfaceType) Reference() string              { return typ.GoName }
+func (typ *goInterfaceType) SelectionSet() ast.SelectionSet { return typ.Selection }
+func (typ *goInterfaceType) GraphQLTypeName() string        { return typ.GraphQLName }
 
 func (typ *goOpaqueType) Unwrap() goType    { return typ }
 func (typ *goSliceType) Unwrap() goType     { return typ.Elem.Unwrap() }
diff --git a/generate/binding_validation.go b/generate/validation.go
similarity index 93%
rename from generate/binding_validation.go
rename to generate/validation.go
index 9f5f8e2..048996a 100644
--- a/generate/binding_validation.go
+++ b/generate/validation.go
@@ -1,7 +1,8 @@
 package generate
 
-// This file is responsible for doing the validation for type-bindings, if they
-// are so configured (see TypeBinding).
+// This file contains helpers to do various bits of validation in the process
+// of converting types to Go, notably, for cases where we need to check that
+// two types match.
 
 import (
 	"fmt"
@@ -17,7 +18,7 @@ import (
 // order, and fragment-structure.  It does not recurse into named fragments, it
 // only checks that their names match.
 //
-// TODO(benkraft): Should we check arguments/directives?
+// If both selection-sets are nil/empty, they compare equal.
 func selectionsMatch(
 	pos *ast.Position,
 	expectedSelectionSet, actualSelectionSet ast.SelectionSet,


Test patch:
diff --git a/generate/testdata/errors/ConflictingTypeNames.go b/generate/testdata/errors/ConflictingTypeNames.go
new file mode 100644
index 0000000..1b9a787
--- /dev/null
+++ b/generate/testdata/errors/ConflictingTypeNames.go
@@ -0,0 +1,10 @@
+package errors
+
+_ = `# @genqlient
+query ConflictingTypeNames {
+  # @genqlient(typename: "T")
+  f { g }
+  # @genqlient(typename: "T")
+  otherF: f { g h }
+}
+`
diff --git a/generate/testdata/errors/ConflictingTypeNames.graphql b/generate/testdata/errors/ConflictingTypeNames.graphql
new file mode 100644
index 0000000..09f0ab9
--- /dev/null
+++ b/generate/testdata/errors/ConflictingTypeNames.graphql
@@ -0,0 +1,6 @@
+query ConflictingTypeNames {
+  # @genqlient(typename: "T")
+  f { g }
+  # @genqlient(typename: "T")
+  otherF: f { g h }
+}
diff --git a/generate/testdata/errors/ConflictingTypeNames.schema.graphql b/generate/testdata/errors/ConflictingTypeNames.schema.graphql
new file mode 100644
index 0000000..b2b5e22
--- /dev/null
+++ b/generate/testdata/errors/ConflictingTypeNames.schema.graphql
@@ -0,0 +1,8 @@
+type Query {
+  f: T
+}
+
+type T {
+  g: String!
+  h: String!
+}
diff --git a/generate/testdata/errors/StructOptionOnObject.graphql b/generate/testdata/errors/StructOptionOnObject.graphql
new file mode 100644
index 0000000..c33137a
--- /dev/null
+++ b/generate/testdata/errors/StructOptionOnObject.graphql
@@ -0,0 +1,6 @@
+query StructOptionOnObject {
+  # @genqlient(struct: true)
+  myObject {
+    f
+  }
+}
diff --git a/generate/testdata/errors/StructOptionOnObject.schema.graphql b/generate/testdata/errors/StructOptionOnObject.schema.graphql
new file mode 100644
index 0000000..029fecc
--- /dev/null
+++ b/generate/testdata/errors/StructOptionOnObject.schema.graphql
@@ -0,0 +1,7 @@
+type Query {
+  myObject: MyObject
+}
+
+type MyObject {
+  f: String!
+}
diff --git a/generate/testdata/errors/StructOptionWithFragments.graphql b/generate/testdata/errors/StructOptionWithFragments.graphql
new file mode 100644
index 0000000..db81e2c
--- /dev/null
+++ b/generate/testdata/errors/StructOptionWithFragments.graphql
@@ -0,0 +1,9 @@
+query StructOptionOnObject {
+  # @genqlient(struct: true)
+  myInterface {
+    f
+    ... on MyObject {
+      g
+    }
+  }
+}
diff --git a/generate/testdata/errors/StructOptionWithFragments.schema.graphql b/generate/testdata/errors/StructOptionWithFragments.schema.graphql
new file mode 100644
index 0000000..18c11b7
--- /dev/null
+++ b/generate/testdata/errors/StructOptionWithFragments.schema.graphql
@@ -0,0 +1,16 @@
+type Query {
+  myInterface: MyInterface
+}
+
+interface MyInterface {
+  f: String!
+}
+
+type MyObject implements MyInterface {
+  f: String!
+  g: String!
+}
+
+type OtherObject implements MyInterface {
+  f: String!
+}
diff --git a/generate/testdata/queries/Recursion.graphql b/generate/testdata/queries/Recursion.graphql
new file mode 100644
index 0000000..2856c9c
--- /dev/null
+++ b/generate/testdata/queries/Recursion.graphql
@@ -0,0 +1,6 @@
+query Recursion($input: RecursiveInput!) {
+  recur(input: $input) {
+    # (sadly, or happily, GraphQL doesn't let us recur infinitely here)
+    rec { rec { rec { id } } }
+  }
+}
diff --git a/generate/testdata/queries/StructOption.graphql b/generate/testdata/queries/StructOption.graphql
new file mode 100644
index 0000000..89391fc
--- /dev/null
+++ b/generate/testdata/queries/StructOption.graphql
@@ -0,0 +1,24 @@
+fragment VideoFields on Video { duration }
+
+# @genqlient(struct: true)
+query StructOption {
+  root {
+    id
+    children {
+      id
+      parent {
+        id
+        children {
+          id
+        }
+        # (it won't apply to this)
+        interfaceChildren: children {
+          id
+          ...VideoFields
+        }
+      }
+    }
+  }
+  # (nor this)
+  user { roles }
+}
diff --git a/generate/testdata/queries/TypeNames.graphql b/generate/testdata/queries/TypeNames.graphql
new file mode 100644
index 0000000..7c7ce55
--- /dev/null
+++ b/generate/testdata/queries/TypeNames.graphql
@@ -0,0 +1,10 @@
+# @genqlient(typename: "Resp")
+query TypeNames {
+  # @genqlient(typename: "User")
+  user { id name }
+  # @genqlient(typename: "Item")
+  randomItem { id name }
+  # (ok to reuse the name as long as they match)
+  # @genqlient(typename: "User")
+  users { id name }
+}
diff --git a/generate/testdata/queries/schema.graphql b/generate/testdata/queries/schema.graphql
index d3c940c..3392352 100644
--- a/generate/testdata/queries/schema.graphql
+++ b/generate/testdata/queries/schema.graphql
@@ -131,6 +131,15 @@ type Topic implements Content {
   schoolGrade: String
 }
 
+input RecursiveInput {
+  rec: [RecursiveInput]
+}
+
+type Recursive {
+  id: ID!
+  rec: Recursive
+}
+
 """Query's description is probably ignored by almost all callers."""
 type Query {
   """user looks up a user by some stuff.
@@ -140,7 +149,7 @@ type Query {
   """
   user(query: UserQueryInput): User
 
-  users(query: [UserQueryInput]): User
+  users(query: [UserQueryInput]): [User]
 
   """usersWithRole looks a user up by role."""
   usersWithRole(role: Role!): [User!]!
@@ -153,6 +162,7 @@ type Query {
   getComplexJunk: ComplexJunk
   listOfListsOfLists: [[[String!]!]!]!
   listOfListsOfListsOfContent: [[[Content!]!]!]!
+  recur(input: RecursiveInput!): Recursive
 }
 
 type Mutation {
diff --git a/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go b/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go
index cb81b5e..4a904f0 100644
--- a/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go
@@ -15,10 +15,10 @@ type OmitEmptyQueryResponse struct {
 	//
 	// See UserQueryInput for what stuff is supported.
 	// If query is null, returns the current user.
-	User         OmitEmptyQueryUser      `json:"user"`
-	Users        OmitEmptyQueryUsersUser `json:"users"`
-	MaybeConvert time.Time               `json:"maybeConvert"`
-	Convert2     time.Time               `json:"convert2"`
+	User         OmitEmptyQueryUser        `json:"user"`
+	Users        []OmitEmptyQueryUsersUser `json:"users"`
+	MaybeConvert time.Time                 `json:"maybeConvert"`
+	Convert2     time.Time                 `json:"convert2"`
 }
 
 // OmitEmptyQueryUser includes the requested fields of the GraphQL type User.
diff --git a/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.go b/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.go
new file mode 100644
index 0000000..02e2852
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.go
@@ -0,0 +1,71 @@
+package test
+
+// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
+
+import (
+	"github.com/Khan/genqlient/graphql"
+	"github.com/Khan/genqlient/internal/testutil"
+)
+
+// RecursionRecurRecursive includes the requested fields of the GraphQL type Recursive.
+type RecursionRecurRecursive struct {
+	Rec RecursionRecurRecursiveRecRecursive `json:"rec"`
+}
+
+// RecursionRecurRecursiveRecRecursive includes the requested fields of the GraphQL type Recursive.
+type RecursionRecurRecursiveRecRecursive struct {
+	Rec RecursionRecurRecursiveRecRecursiveRecRecursive `json:"rec"`
+}
+
+// RecursionRecurRecursiveRecRecursiveRecRecursive includes the requested fields of the GraphQL type Recursive.
+type RecursionRecurRecursiveRecRecursiveRecRecursive struct {
+	Rec RecursionRecurRecursiveRecRecursiveRecRecursiveRecRecursive `json:"rec"`
+}
+
+// RecursionRecurRecursiveRecRecursiveRecRecursiveRecRecursive includes the requested fields of the GraphQL type Recursive.
+type RecursionRecurRecursiveRecRecursiveRecRecursiveRecRecursive struct {
+	Id testutil.ID `json:"id"`
+}
+
+// RecursionResponse is returned by Recursion on success.
+type RecursionResponse struct {
+	Recur RecursionRecurRecursive `json:"recur"`
+}
+
+type RecursiveInput struct {
+	Rec []RecursiveInput `json:"rec"`
+}
+
+func Recursion(
+	client graphql.Client,
+	input RecursiveInput,
+) (*RecursionResponse, error) {
+	variables := map[string]interface{}{
+		"input": input,
+	}
+
+	var err error
+
+	var retval RecursionResponse
+	err = client.MakeRequest(
+		nil,
+		"Recursion",
+		`
+query Recursion ($input: RecursiveInput!) {
+	recur(input: $input) {
+		rec {
+			rec {
+				rec {
+					id
+				}
+			}
+		}
+	}
+}
+`,
+		&retval,
+		variables,
+	)
+	return &retval, err
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.json b/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.json
new file mode 100644
index 0000000..44fb4fb
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-Recursion.graphql-Recursion.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "Recursion",
+      "query": "\nquery Recursion ($input: RecursiveInput!) {\n\trecur(input: $input) {\n\t\trec {\n\t\t\trec {\n\t\t\t\trec {\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n",
+      "sourceLocation": "testdata/queries/Recursion.graphql"
+    }
+  ]
+}
diff --git a/generate/testdata/snapshots/TestGenerate-StructOption.graphql-StructOption.graphql.go b/generate/testdata/snapshots/TestGenerate-StructOption.graphql-StructOption.graphql.go
new file mode 100644
index 0000000..ff55649
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-StructOption.graphql-StructOption.graphql.go
@@ -0,0 +1,296 @@
+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"
+)
+
+// Role is a type a user may have.
+type Role string
+
+const (
+	// What is a student?
+	//
+	// A student is primarily a person enrolled in a school or other educational institution and who is under learning with goals of acquiring knowledge, developing professions and achieving employment at desired field. In the broader sense, a student is anyone who applies themselves to the intensive intellectual engagement with some matter necessary to master it as part of some practical affair in which such mastery is basic or decisive.
+	//
+	// (from [Wikipedia](https://en.wikipedia.org/wiki/Student))
+	RoleStudent Role = "STUDENT"
+	// Teacher is a teacher, who teaches the students.
+	RoleTeacher Role = "TEACHER"
+)
+
+// StructOptionResponse is returned by StructOption on success.
+type StructOptionResponse struct {
+	Root StructOptionRootTopic `json:"root"`
+	// user looks up a user by some stuff.
+	//
+	// See UserQueryInput for what stuff is supported.
+	// If query is null, returns the current user.
+	User StructOptionUser `json:"user"`
+}
+
+// StructOptionRootTopic includes the requested fields of the GraphQL type Topic.
+type StructOptionRootTopic struct {
+	// ID is documented in the Content interface.
+	Id       testutil.ID                            `json:"id"`
+	Children []StructOptionRootTopicChildrenContent `json:"children"`
+}
+
+// StructOptionRootTopicChildrenContent includes the requested fields of the GraphQL type Content.
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type StructOptionRootTopicChildrenContent struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id     testutil.ID                                     `json:"id"`
+	Parent StructOptionRootTopicChildrenContentParentTopic `json:"parent"`
+}
+
+// StructOptionRootTopicChildrenContentParentTopic includes the requested fields of the GraphQL type Topic.
+type StructOptionRootTopicChildrenContentParentTopic struct {
+	// ID is documented in the Content interface.
+	Id                testutil.ID                                                               `json:"id"`
+	Children          []StructOptionRootTopicChildrenContentParentTopicChildrenContent          `json:"children"`
+	InterfaceChildren []StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent `json:"-"`
+}
+
+func (v *StructOptionRootTopicChildrenContentParentTopic) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*StructOptionRootTopicChildrenContentParentTopic
+		InterfaceChildren []json.RawMessage `json:"interfaceChildren"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.StructOptionRootTopicChildrenContentParentTopic = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.InterfaceChildren
+		raw := firstPass.InterfaceChildren
+		*target = make(
+			[]StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent,
+			len(raw))
+		for i, raw := range raw {
+			target := &(*target)[i]
+			err = __unmarshalStructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent(
+				target, raw)
+			if err != nil {
+				return fmt.Errorf(
+					"Unable to unmarshal StructOptionRootTopicChildrenContentParentTopic.InterfaceChildren: %w", err)
+			}
+		}
+	}
+	return nil
+}
+
+// StructOptionRootTopicChildrenContentParentTopicChildrenContent includes the requested fields of the GraphQL type Content.
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type StructOptionRootTopicChildrenContentParentTopicChildrenContent struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id testutil.ID `json:"id"`
+}
+
+// StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenArticle includes the requested fields of the GraphQL type Article.
+type StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenArticle struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id testutil.ID `json:"id"`
+}
+
+// StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent includes the requested fields of the GraphQL interface Content.
+//
+// StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent is implemented by the following types:
+// StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenArticle
+// StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo
+// StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenTopic
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent interface {
+	implementsGraphQLInterfaceStructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent()
+	// GetTypename returns the receiver's concrete GraphQL type-name (see interface doc for possible values).
+	GetTypename() string
+	// GetId returns the interface-field "id" from its implementation.
+	// The GraphQL interface field's documentation follows.
+	//
+	// ID is the identifier of the content.
+	GetId() testutil.ID
+}
+
+func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenArticle) implementsGraphQLInterfaceStructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent.
+func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenArticle) GetTypename() string {
+	return v.Typename
+}
+
+// GetId is a part of, and documented with, the interface StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent.
+func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenArticle) GetId() testutil.ID {
+	return v.Id
+}
+
+func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo) implementsGraphQLInterfaceStructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent.
+func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo) GetTypename() string {
+	return v.Typename
+}
+
+// GetId is a part of, and documented with, the interface StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent.
+func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo) GetId() testutil.ID {
+	return v.Id
+}
+
+func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenTopic) implementsGraphQLInterfaceStructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent() {
+}
+
+// GetTypename is a part of, and documented with, the interface StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent.
+func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenTopic) GetTypename() string {
+	return v.Typename
+}
+
+// GetId is a part of, and documented with, the interface StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent.
+func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenTopic) GetId() testutil.ID {
+	return v.Id
+}
+
+func __unmarshalStructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent(v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent, 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(StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent: "%v"`, tn.TypeName)
+	}
+}
+
+// StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenTopic includes the requested fields of the GraphQL type Topic.
+type StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenTopic struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id testutil.ID `json:"id"`
+}
+
+// StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo includes the requested fields of the GraphQL type Video.
+type StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id          testutil.ID `json:"id"`
+	VideoFields `json:"-"`
+}
+
+func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	err = json.Unmarshal(
+		b, &v.VideoFields)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// StructOptionUser includes the requested fields of the GraphQL type User.
+// The GraphQL type's documentation follows.
+//
+// A User is a user!
+type StructOptionUser struct {
+	Roles []Role `json:"roles"`
+}
+
+// VideoFields includes the GraphQL fields of Video requested by the fragment VideoFields.
+type VideoFields struct {
+	Duration int `json:"duration"`
+}
+
+func StructOption(
+	client graphql.Client,
+) (*StructOptionResponse, error) {
+	var err error
+
+	var retval StructOptionResponse
+	err = client.MakeRequest(
+		nil,
+		"StructOption",
+		`
+query StructOption {
+	root {
+		id
+		children {
+			__typename
+			id
+			parent {
+				id
+				children {
+					__typename
+					id
+				}
+				interfaceChildren: children {
+					__typename
+					id
+					... VideoFields
+				}
+			}
+		}
+	}
+	user {
+		roles
+	}
+}
+fragment VideoFields on Video {
+	duration
+}
+`,
+		&retval,
+		nil,
+	)
+	return &retval, err
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-StructOption.graphql-StructOption.graphql.json b/generate/testdata/snapshots/TestGenerate-StructOption.graphql-StructOption.graphql.json
new file mode 100644
index 0000000..3d3aa7a
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-StructOption.graphql-StructOption.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "StructOption",
+      "query": "\nquery StructOption {\n\troot {\n\t\tid\n\t\tchildren {\n\t\t\t__typename\n\t\t\tid\n\t\t\tparent {\n\t\t\t\tid\n\t\t\t\tchildren {\n\t\t\t\t\t__typename\n\t\t\t\t\tid\n\t\t\t\t}\n\t\t\t\tinterfaceChildren: children {\n\t\t\t\t\t__typename\n\t\t\t\t\tid\n\t\t\t\t\t... VideoFields\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tuser {\n\t\troles\n\t}\n}\nfragment VideoFields on Video {\n\tduration\n}\n",
+      "sourceLocation": "testdata/queries/StructOption.graphql"
+    }
+  ]
+}
diff --git a/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go
new file mode 100644
index 0000000..4445b53
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go
@@ -0,0 +1,205 @@
+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"
+)
+
+// Item includes the requested fields of the GraphQL interface Content.
+//
+// Item is implemented by the following types:
+// ItemArticle
+// ItemVideo
+// ItemTopic
+// The GraphQL type's documentation follows.
+//
+// Content is implemented by various types like Article, Video, and Topic.
+type Item interface {
+	implementsGraphQLInterfaceItem()
+	// 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 *ItemArticle) implementsGraphQLInterfaceItem() {}
+
+// GetTypename is a part of, and documented with, the interface Item.
+func (v *ItemArticle) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface Item.
+func (v *ItemArticle) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface Item.
+func (v *ItemArticle) GetName() string { return v.Name }
+
+func (v *ItemVideo) implementsGraphQLInterfaceItem() {}
+
+// GetTypename is a part of, and documented with, the interface Item.
+func (v *ItemVideo) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface Item.
+func (v *ItemVideo) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface Item.
+func (v *ItemVideo) GetName() string { return v.Name }
+
+func (v *ItemTopic) implementsGraphQLInterfaceItem() {}
+
+// GetTypename is a part of, and documented with, the interface Item.
+func (v *ItemTopic) GetTypename() string { return v.Typename }
+
+// GetId is a part of, and documented with, the interface Item.
+func (v *ItemTopic) GetId() testutil.ID { return v.Id }
+
+// GetName is a part of, and documented with, the interface Item.
+func (v *ItemTopic) GetName() string { return v.Name }
+
+func __unmarshalItem(v *Item, 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(ItemArticle)
+		return json.Unmarshal(m, *v)
+	case "Video":
+		*v = new(ItemVideo)
+		return json.Unmarshal(m, *v)
+	case "Topic":
+		*v = new(ItemTopic)
+		return json.Unmarshal(m, *v)
+	case "":
+		return fmt.Errorf(
+			"Response was missing Content.__typename")
+	default:
+		return fmt.Errorf(
+			`Unexpected concrete type for Item: "%v"`, tn.TypeName)
+	}
+}
+
+// ItemArticle includes the requested fields of the GraphQL type Article.
+type ItemArticle struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// ItemTopic includes the requested fields of the GraphQL type Topic.
+type ItemTopic struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// ItemVideo includes the requested fields of the GraphQL type Video.
+type ItemVideo struct {
+	Typename string `json:"__typename"`
+	// ID is the identifier of the content.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+// Resp is returned by TypeNames on success.
+type Resp 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       User   `json:"user"`
+	RandomItem Item   `json:"-"`
+	Users      []User `json:"users"`
+}
+
+func (v *Resp) UnmarshalJSON(b []byte) error {
+
+	var firstPass struct {
+		*Resp
+		RandomItem json.RawMessage `json:"randomItem"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.Resp = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		target := &v.RandomItem
+		raw := firstPass.RandomItem
+		err = __unmarshalItem(
+			target, raw)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal Resp.RandomItem: %w", err)
+		}
+	}
+	return nil
+}
+
+// User includes the requested fields of the GraphQL type User.
+// The GraphQL type's documentation follows.
+//
+// A User is a user!
+type User struct {
+	// id is the user's ID.
+	//
+	// It is stable, unique, and opaque, like all good IDs.
+	Id   testutil.ID `json:"id"`
+	Name string      `json:"name"`
+}
+
+func TypeNames(
+	client graphql.Client,
+) (*Resp, error) {
+	var err error
+
+	var retval Resp
+	err = client.MakeRequest(
+		nil,
+		"TypeNames",
+		`
+query TypeNames {
+	user {
+		id
+		name
+	}
+	randomItem {
+		__typename
+		id
+		name
+	}
+	users {
+		id
+		name
+	}
+}
+`,
+		&retval,
+		nil,
+	)
+	return &retval, err
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.json b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.json
new file mode 100644
index 0000000..3d8e0f3
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "TypeNames",
+      "query": "\nquery TypeNames {\n\tuser {\n\t\tid\n\t\tname\n\t}\n\trandomItem {\n\t\t__typename\n\t\tid\n\t\tname\n\t}\n\tusers {\n\t\tid\n\t\tname\n\t}\n}\n",
+      "sourceLocation": "testdata/queries/TypeNames.graphql"
+    }
+  ]
+}
diff --git a/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-go b/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-go
new file mode 100644
index 0000000..ff46213
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-go
@@ -0,0 +1,1 @@
+invalid Go file testdata/errors/ConflictingTypeNames.go: testdata/errors/ConflictingTypeNames.go:3:1: expected declaration, found _
diff --git a/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-graphql b/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-graphql
new file mode 100644
index 0000000..ca5d185
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerateErrors-ConflictingTypeNames-graphql
@@ -0,0 +1,1 @@
+testdata/errors/ConflictingTypeNames.schema.graphql:2: conflicting definition for T; this can indicate either a genqlient internal error, a conflict between user-specified type-names, or some very tricksy GraphQL field/type names: expected 2 fields, got 1
diff --git a/generate/testdata/snapshots/TestGenerateErrors-StructOptionOnObject-graphql b/generate/testdata/snapshots/TestGenerateErrors-StructOptionOnObject-graphql
new file mode 100644
index 0000000..65a71dd
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerateErrors-StructOptionOnObject-graphql
@@ -0,0 +1,1 @@
+testdata/errors/StructOptionOnObject.graphql:3: struct is only applicable to interface-typed fields
diff --git a/generate/testdata/snapshots/TestGenerateErrors-StructOptionWithFragments-graphql b/generate/testdata/snapshots/TestGenerateErrors-StructOptionWithFragments-graphql
new file mode 100644
index 0000000..35de573
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerateErrors-StructOptionWithFragments-graphql
@@ -0,0 +1,1 @@
+testdata/errors/StructOptionWithFragments.graphql:3: struct is not allowed for types with fragments