Repository: Khan/genqlient
Pull request #104 (2021-09-17 19:52:56)

Add support for binding with a custom marshal/unmarshal function
## Summary:
This is useful if you want to bind to a type you don't control (or use
for other things) but need different serialization than its default.
This is a feature gqlgen has and we've found it very useful.  For
example, in webapp we want to bind `DateTime` to `time.Time`, but its
default serialization is not compatible with Python, so currently we
have to bind to a wrapper type and cast all over the place, which is
exactly the sort of boilerplate genqlient is supposed to avoid.

For unmarshaling, the implementation basically just follows the existing
support for abstract types; instead of calling our own generated
helper, we now call your specified function.  This required some
refactoring to abstract the handling of custom unmarshalers generally
from abstract types specifically, and to wire in not only the
unmarshaler-name but also the `generator` (in order to compute the right
import alias).

For marshaling, I had to implement all that stuff over again; it's
mostly parallel to unmarshaling (and I made a few minor changes to
unmarshaling to make the two more parallel).  Luckily, after #103 I at
least only had to do it once, rather than implementing the same
functionality for arguments and for input-type fields.  It was still
quite a bit of code; I didn't try to be quite as completionist about the
tests as with unmarshal but still had to add a few.

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

## Test plan:
make check



Patch:
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index f9e6760..d49a2be 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -23,10 +23,12 @@ When releasing a new version:
 ### Breaking changes:
 
 - The [`graphql.Client`](https://pkg.go.dev/github.com/Khan/genqlient/graphql#Client) interface now accepts `variables interface{}` (containing a JSON-marshalable value) rather than `variables map[string]interface{}`.  Clients implementing the interface themselves will need to change the signature; clients who simply call `graphql.NewClient` are unaffected.
-- genqlient's handling of the `omitempty` option has changed to match that of `encoding/json`, from which it had inadvertently differed.  In particular, this means struct-typed arguments with `# @genqlient(omitempty: true)` will no longer be omitted if they are the zero value.  (Struct-pointers are still omitted if nil, so adding `pointer: true` will typically work fine.)
+- genqlient's handling of the `omitempty` option has changed to match that of `encoding/json`, from which it had inadvertently differed.  In particular, this means struct-typed arguments with `# @genqlient(omitempty: true)` will no longer be omitted if they are the zero value.  (Struct-pointers are still omitted if nil, so adding `pointer: true` will typically work fine.  It's also now possible to use a custom marshaler to explicitly map zero to null.)
 
 ### New features:
 
+- The new `bindings.marshaler` and `bindings.unmarshaler` options in `genqlient.yaml` allow binding to a type without using its standard JSON serialization; see the [documentation](genqlient.yaml) for details.
+
 ### Bug fixes:
 
 - The `omitempty` option now works correctly for struct- and map-typed variables, matching `encoding/json`, which is to say it never omits structs, and omits empty maps. (#43)
diff --git a/docs/genqlient.yaml b/docs/genqlient.yaml
index 8dc540b..b909d61 100644
--- a/docs/genqlient.yaml
+++ b/docs/genqlient.yaml
@@ -104,6 +104,42 @@ bindings:
     # - a nonstandard way of spelling those, (interface {/* hi */},
     #   map[  string      ]T)
     type: time.Time
+    # Optionally, the fully-qualified name of the function to use when
+    # marshaling this type.
+    #
+    # This is useful when you want to bind to a standard type, but use
+    # nonstandard marshaling, for example when making requests to a server
+    # that's not compatible with Go's default time format. It is only used for
+    # types passed as arguments, i.e. input types, scalars, and enums.
+    #
+    # The function should have a signature similar to json.Marshal, i.e., it
+    # will be passed one argument which will be a pointer to a value of the
+    # given type, and must return two values: the JSON as a `[]byte`, and an
+    # error. For example, you might specify
+    #  unmarshaler: github.com/you/yourpkg.MarshalMyType
+    # and that function is defined as e.g.:
+    #  func MarshalMyType(v *MyType) ([]byte, error)
+    #
+    # Note that the `omitempty` option is ignored for types with custom
+    # marshalers; the custom marshaler can of course choose to map any value it
+    # wishes to `"null"` which in GraphQL has the same effect.
+    # 
+    # The default is to use ordinary JSON-marshaling.
+    marshaler: github.com/you/yourpkg.MarshalDateTime
+    # Optionally, the fully-qualified name of the function to use when
+    # unmarshaling this type.
+    #
+    # This is similar to marshaler, above, but for unmarshaling.  The specified
+    # function should have a signature similar to json.Unmarshal, i.e., it will
+    # be passed two arguments, a []byte of JSON to unmarshal and a pointer to a
+    # value of the given type, and must return an error.  For example, you
+    # might specify
+    #  unmarshaler: github.com/you/yourpkg.UnmarshalMyType
+    # and that function is defined as e.g.:
+    #  func UnmarshalMyType(b []byte, v *MyType) error
+    # 
+    # The default is to use ordinary JSON-unmarshaling.
+    unmarshaler: github.com/you/yourpkg.UnmarshalDateTime
 
   # To bind an object type:
   MyType:
@@ -124,3 +160,4 @@ bindings:
     # or something, if you want to say, for example, that you have to request
     # certain fields but others are optional.
     expect_exact_fields: "{ id name }"
+    # unmarshaler and marshaler are also valid here, see above for details.
diff --git a/docs/genqlient_directive.graphql b/docs/genqlient_directive.graphql
index dc56a2b..615a66e 100644
--- a/docs/genqlient_directive.graphql
+++ b/docs/genqlient_directive.graphql
@@ -44,7 +44,8 @@ directive genqlient(
   # which will pass {"arg": null} to GraphQL if arg is "", and the actual
   # value otherwise.
   #
-  # Only applicable to arguments of nullable types.
+  # Only applicable to arguments of nullable types.  Ignored for types with
+  # custom marshalers (see their documentation in genqlient.yaml for details).
   omitempty: Boolean
 
   # If set, this argument or field will use a pointer type in Go.  Response
diff --git a/generate/config.go b/generate/config.go
index eb8112f..5aa11ca 100644
--- a/generate/config.go
+++ b/generate/config.go
@@ -44,6 +44,8 @@ type Config struct {
 type TypeBinding struct {
 	Type              string `yaml:"type"`
 	ExpectExactFields string `yaml:"expect_exact_fields"`
+	Marshaler         string `yaml:"marshaler"`
+	Unmarshaler       string `yaml:"unmarshaler"`
 }
 
 // ValidateAndFillDefaults ensures that the configuration is valid, and fills
diff --git a/generate/convert.go b/generate/convert.go
index 2398166..1084e10 100644
--- a/generate/convert.go
+++ b/generate/convert.go
@@ -125,6 +125,7 @@ func (g *generator) convertOperation(
 		},
 		Fields:    fields,
 		Selection: operation.SelectionSet,
+		Generator: g,
 	}
 
 	return g.addType(goType, goType.GoName, operation.Position)
@@ -188,6 +189,7 @@ func (g *generator) convertArguments(
 			// fake name, used by addType
 			GraphQLName: name,
 		},
+		Generator: g,
 	}
 	goTypAgain, err := g.addType(goTyp, goTyp.GoName, operation.Position)
 	if err != nil {
@@ -217,7 +219,9 @@ func (g *generator) convertType(
 	localBinding := options.Bind
 	if localBinding != "" && localBinding != "-" {
 		goRef, err := g.ref(localBinding)
-		return &goOpaqueType{goRef, typ.Name()}, err
+		// TODO(benkraft): Add syntax to specify a custom (un)marshaler, if
+		// it proves useful.
+		return &goOpaqueType{GoRef: goRef, GraphQLName: typ.Name()}, err
 	}
 
 	if typ.Elem != nil {
@@ -269,11 +273,16 @@ func (g *generator) convertDefinition(
 			}
 		}
 		goRef, err := g.ref(globalBinding.Type)
-		return &goOpaqueType{goRef, def.Name}, err
+		return &goOpaqueType{
+			GoRef:       goRef,
+			GraphQLName: def.Name,
+			Marshaler:   globalBinding.Marshaler,
+			Unmarshaler: globalBinding.Unmarshaler,
+		}, err
 	}
 	goBuiltinName, ok := builtinTypes[def.Name]
 	if ok {
-		return &goOpaqueType{goBuiltinName, def.Name}, nil
+		return &goOpaqueType{GoRef: goBuiltinName, GraphQLName: def.Name}, nil
 	}
 
 	// Determine the name to use for this type.
@@ -337,6 +346,7 @@ func (g *generator) convertDefinition(
 			Fields:          fields,
 			Selection:       selectionSet,
 			descriptionInfo: desc,
+			Generator:       g,
 		}
 		return g.addType(goType, goType.GoName, pos)
 
@@ -346,6 +356,7 @@ func (g *generator) convertDefinition(
 			Fields:          make([]*goStructField, len(def.Fields)),
 			descriptionInfo: desc,
 			IsInput:         true,
+			Generator:       g,
 		}
 		// To handle recursive types, we need to add the type to the type-map
 		// *before* converting its fields.
@@ -700,6 +711,7 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy
 			Fields:          fields,
 			Selection:       fragment.SelectionSet,
 			descriptionInfo: desc,
+			Generator:       g,
 		}
 		g.typeMap[fragment.Name] = goType
 		return goType, nil
@@ -729,6 +741,7 @@ func (g *generator) convertNamedFragment(fragment *ast.FragmentDefinition) (goTy
 				Fields:          implFields,
 				Selection:       fragment.SelectionSet,
 				descriptionInfo: implDesc,
+				Generator:       g,
 			}
 			goType.Implementations[i] = implTyp
 			g.typeMap[implTyp.GoName] = implTyp
diff --git a/generate/marshal.go.tmpl b/generate/marshal.go.tmpl
new file mode 100644
index 0000000..10d595e
--- /dev/null
+++ b/generate/marshal.go.tmpl
@@ -0,0 +1,52 @@
+{{/* See unmarshal.go.tmpl for more on how this works; this is mostly just
+     parallel (and simplified -- we don't need to handle embedding). */}}
+
+func (v *{{.GoName}}) MarshalJSON() ([]byte, error) {
+    {{/* We do the two passes in the opposite order of unmarshal: first, we
+         marshal the special fields, then we assign those to the wrapper struct
+         and finish marshaling the whole object.  But first we set up the
+         object for the second part, so we can assign to it as we go. */}}
+    var fullObject struct{
+        *{{.GoName}}
+        {{range .Fields -}}
+        {{if .NeedsMarshaler -}}
+        {{.GoName}} {{repeat .GoType.SliceDepth "[]"}}{{ref "encoding/json.RawMessage"}} `json:"{{.JSONName}}"`
+        {{end -}}
+        {{end -}}
+        {{ref "github.com/Khan/genqlient/graphql.NoMarshalJSON"}}
+    }
+    fullObject.{{.GoName}} = v
+
+    {{range $field := .Fields -}}
+    {{if $field.NeedsMarshaler -}}
+    {
+        {{/* Here dst is the json.RawMessage, and src is the Go type */}}
+        dst := &fullObject.{{$field.GoName}}
+        src := v.{{$field.GoName}}
+        {{range $i := intRange $field.GoType.SliceDepth -}}
+        *dst = make(
+            {{repeat (sub $field.GoType.SliceDepth $i) "[]"}}{{ref "encoding/json.RawMessage"}},
+            len(src))
+        for i, src := range src {
+            dst := &(*dst)[i]
+        {{end -}}
+        var err error
+        *dst, err = {{$field.Marshaler $.Generator}}(
+            {{/* src is a pointer to the struct-field (or field-element, etc.).
+                 We want to pass a pointer to the type you specified, so if
+                 there's a pointer on the field that's exactly what we want,
+                 and if not we need to take the address. */ -}}
+            {{if not $field.GoType.IsPointer}}&{{end}}src)
+        if err != nil {
+            return nil, fmt.Errorf(
+                "Unable to marshal {{$.GoName}}.{{$field.GoName}}: %w", err)
+        }
+        {{range $i := intRange $field.GoType.SliceDepth -}}
+        }
+        {{end -}}
+    }
+    {{end -}}
+    {{end}}
+
+    return {{ref "encoding/json.Marshal"}}(&fullObject)
+}
diff --git a/generate/types.go b/generate/types.go
index 3b70d84..c758fdd 100644
--- a/generate/types.go
+++ b/generate/types.go
@@ -61,8 +61,9 @@ 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
-		GraphQLName string
+		GoRef                  string
+		GraphQLName            string
+		Marshaler, Unmarshaler string
 	}
 	// goSliceType represents the Go type []Elem, used to represent GraphQL
 	// list types.
@@ -132,6 +133,7 @@ type goStructType struct {
 	IsInput   bool
 	Selection ast.SelectionSet
 	descriptionInfo
+	Generator *generator // for the convenience of the template
 }
 
 type goStructField struct {
@@ -156,10 +158,74 @@ func (field *goStructField) IsEmbedded() bool {
 	return field.GoName == ""
 }
 
+// unmarshaler returns:
+// - the name of the function to use to unmarshal this field
+// - true if this is a fully-qualified name (false if it is a package-local
+//   unqualified name)
+// - true if we need to generate an unmarshaler at all, false if the default
+//   behavior will suffice
+func (field *goStructField) unmarshaler() (qualifiedName string, needsImport bool, needsUnmarshaler bool) {
+	switch typ := field.GoType.Unwrap().(type) {
+	case *goOpaqueType:
+		if typ.Unmarshaler != "" {
+			return typ.Unmarshaler, true, true
+		}
+	case *goInterfaceType:
+		return "__unmarshal" + typ.Reference(), false, true
+	}
+	return "encoding/json.Unmarshal", true, field.IsEmbedded()
+}
+
+// NeedsUnmarshaler returns true if this field needs special handling when
+// unmarshaling, e.g. if it's of interface type, embedded, or has a
+// user-specified custom unmarshaler.
+func (field *goStructField) NeedsUnmarshaler() bool {
+	_, _, ok := field.unmarshaler()
+	return ok
+}
+
+// Unmarshaler returns the Go name of the function to use to unmarshal this
+// field (which may be "json.Unmarshal" if there's not a special one).
+func (field *goStructField) Unmarshaler(g *generator) (string, error) {
+	name, needsImport, _ := field.unmarshaler()
+	if needsImport {
+		return g.ref(name)
+	}
+	return name, nil
+}
+
+// marshaler returns:
+// - the fully-qualified name of the function to use to marshal this field
+// - true if we need to generate an marshaler at all, false if the default
+//   behavior will suffice
+func (field *goStructField) marshaler() (qualifiedName string, needsMarshaler bool) {
+	// (there are no interfaces on the input side)
+	opaque, ok := field.GoType.Unwrap().(*goOpaqueType)
+	if ok && opaque.Marshaler != "" {
+		return opaque.Marshaler, true
+	}
+	return "encoding/json.Marshal", field.IsEmbedded()
+}
+
+// NeedsMarshaler returns true if this field needs special handling when
+// marshaling, e.g. if it has a user-specified custom marshaler.
+func (field *goStructField) NeedsMarshaler() bool {
+	_, ok := field.marshaler()
+	return ok
+}
+
+// Marshaler returns the Go name of the function to use to marshal this
+// field (which may be "json.Marshal" if there's not a special one).
+func (field *goStructField) Marshaler(g *generator) (string, error) {
+	name, _ := field.marshaler()
+	// Unlike unmarshaler, we never have a local name, and always need g.ref.
+	return g.ref(name)
+}
+
 func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
 	writeDescription(w, structDescription(typ))
 
-	needUnmarshaler := false
+	needUnmarshaler, needMarshaler := false, false
 	fmt.Fprintf(w, "type %s struct {\n", typ.GoName)
 	for _, field := range typ.Fields {
 		writeDescription(w, field.Description)
@@ -168,24 +234,24 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
 			jsonTag += ",omitempty"
 		}
 		jsonTag += `"`
-		if field.IsAbstract() {
-			// abstract types are handled in our UnmarshalJSON (see below)
+		if !typ.IsInput && field.NeedsUnmarshaler() {
+			// certain types are handled in our UnmarshalJSON (see below)
 			needUnmarshaler = true
 			jsonTag = `"-"`
 		}
-		if field.IsEmbedded() {
-			// embedded fields also need UnmarshalJSON handling (see below)
-			needUnmarshaler = true
-			fmt.Fprintf(w, "\t%s `json:\"-\"`\n", field.GoType.Unwrap().Reference())
-		} else {
-			fmt.Fprintf(w, "\t%s %s `json:%s`\n",
-				field.GoName, field.GoType.Reference(), jsonTag)
+		if typ.IsInput && field.NeedsMarshaler() {
+			needMarshaler = true
+			jsonTag = `"-"`
 		}
+		// Note for embedded types field.GoName is "", which produces the code
+		// we want!
+		fmt.Fprintf(w, "\t%s %s `json:%s`\n",
+			field.GoName, field.GoType.Reference(), jsonTag)
 	}
 	fmt.Fprintf(w, "}\n")
 
-	// Now, if needed, write the unmarshaler.  We need one if we have any
-	// interface-typed fields, or any embedded fields.
+	// Now, if needed, write the marshaler/unmarshaler.  We need one if we have
+	// any interface-typed fields, or any embedded fields.
 	//
 	// For interface-typed fields, ideally we'd write an UnmarshalJSON method
 	// on the field, but you can't add a method to an interface.  So we write a
@@ -204,13 +270,28 @@ func (typ *goStructType) WriteDefinition(w io.Writer, g *generator) error {
 	// JSON library will only fill one of those (the least-nested one); we want
 	// to fill them all.
 	//
+	// For fields with a custom marshaler or unmarshaler, we do basically the
+	// same thing as interface-typed fields, except the user has defined the
+	// helper.
+	//
+	// Note that in all cases we need only write an unmarshaler if this is an
+	// input type, and a marshaler if it's an output type.
+	//
 	// TODO(benkraft): If/when proposal #5901 is implemented (Go 1.18 at the
 	// earliest), we may be able to do some of this a simpler way.
-	if !needUnmarshaler {
-		return nil
+	if needUnmarshaler {
+		err := g.render("unmarshal.go.tmpl", w, typ)
+		if err != nil {
+			return err
+		}
 	}
-
-	return g.render("unmarshal.go.tmpl", w, typ)
+	if needMarshaler {
+		err := g.render("marshal.go.tmpl", w, typ)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
 }
 
 func (typ *goStructType) Reference() string              { return typ.GoName }
diff --git a/generate/unmarshal.go.tmpl b/generate/unmarshal.go.tmpl
index 97ef887..14a08b9 100644
--- a/generate/unmarshal.go.tmpl
+++ b/generate/unmarshal.go.tmpl
@@ -2,23 +2,25 @@
      UnmarshalJSON from the function it follows) */}}
 
 func (v *{{.GoName}}) UnmarshalJSON(b []byte) error {
-    {{/* We want to specially handle the abstract or embedded fields, but
-         unmarshal everything else normally.  To handle abstract fields,
-         first we unmarshal them into a json.RawMessage, and then handle those
-         further, below.  Embedded fields we just unmarshal directly into the
-         embedded value.  For the rest, we just want to call json.Unmarshal.
-         But if we do that naively on a value of type `.Type`, it will call
-         this function again, and recurse infinitely.  So we make a wrapper
-         type which embeds both this type and NoUmnarshalJSON, which prevents
-         either's UnmarshalJSON method from being promoted.  For more on why
-         this is so difficult, see
+    {{/* Standard convention for unmarshalers is to no-op on null. */}}
+    if string(b) == "null" {
+        return nil
+    }
+
+    {{/* We want to specially handle certain fields, but unmarshal everything
+         else normally.  To handle abstract fields and fields with custom
+         unmarshalers, first we unmarshal them into a json.RawMessage, and then
+         handle those further, below.  Embedded fields don't need the
+         json.RawMessage; we just use our input again.  Either way, we first
+         want to call json.Unmarshal on the receiver (v).  But if we do that
+         naively on a value of type `.Type`, it will call this function again,
+         and recurse infinitely.  So we make a wrapper type which embeds both
+         this type and NoUmnarshalJSON, which prevents either's UnmarshalJSON
+         method from being promoted.  For more on why this is so difficult, see
          https://github.com/benjaminjkraft/notes/blob/master/go-json-interfaces.md.
          (Note there are a few different ways "hide" the method, but this one
          seems to be the best option that works if this type has embedded types
          with UnmarshalJSON methods.)
-         TODO(benkraft)): Ensure `{{.Type}}Wrapper` won't collide with any
-         other type we need.  (For the most part it being locally-scoped saves
-         us; it's not clear if this can be a problem in practice.)
     */}}
 
     {{/* TODO(benkraft): Omit/simplify the first pass if all fields are
@@ -26,7 +28,7 @@ func (v *{{.GoName}}) UnmarshalJSON(b []byte) error {
     var firstPass struct{
         *{{.GoName}}
         {{range .Fields -}}
-        {{if and .IsAbstract (not .IsEmbedded) -}}
+        {{if and .NeedsUnmarshaler (not .IsEmbedded) -}}
         {{.GoName}} {{repeat .GoType.SliceDepth "[]"}}{{ref "encoding/json.RawMessage"}} `json:"{{.JSONName}}"`
         {{end -}}
         {{end -}}
@@ -43,77 +45,71 @@ func (v *{{.GoName}}) UnmarshalJSON(b []byte) error {
 
     {{/* Now, handle the fields needing special handling. */}}
     {{range $field := .Fields -}}
+    {{if $field.NeedsUnmarshaler -}}
     {{if $field.IsEmbedded -}}
     {{/* Embedded fields are easier: we just unmarshal the same input into
          them.  (They're also easier because they can't be lists, since they
          arise from GraphQL fragment spreads.) */ -}}
-    {{if $field.IsAbstract -}}
-    {{/* Except if they're both abstract and embedded, in which case we need to
-         call the unmarshal-helper instead.  Luckily, we don't embed
-         slice-typed fields, so we don't need the full generality we handle
-         below. */ -}}
-    err = __unmarshal{{$field.GoType.Unwrap.Reference}}(
+    err = {{$field.Unmarshaler $.Generator}}(
         b, &v.{{$field.GoType.Unwrap.Reference}})
-    {{else -}}
-    err = json.Unmarshal(
-        b, &v.{{$field.GoType.Unwrap.Reference}})
-    {{end -}}{{/* inner if .IsAbstract */ -}}
     if err != nil {
         return err
     }
-    {{else if $field.IsAbstract -}}
-    {{/* First, for abstract fields, call the unmarshal-helper.
-         This gets a little complicated because we may have a slice field.
-         So what we do is basically, for each field of type `[][]...[]MyType`:
+    {{else -}}
+    {{/* For other fields (abstract or custom unmarshaler), first, call the
+         unmarshaler (our unmarshal-helper, or the user-specified one,
+         respectively).  This gets a little complicated because we may have
+         a slice field.  So what we do is basically, for each field of type
+         `[][]...[]MyType`:
 
-            target := &v.MyField      // *[][]...[]MyType
-            raw := firstPass.MyField  // [][]...[]json.RawMessage
+            dst := &v.MyField      // *[][]...[]MyType
+            src := firstPass.MyField  // [][]...[]json.RawMessage
 
             // repeat the following three lines n times; each time, inside
-            // the loop we have one less layer of slice on raw and target
-            *target = make([][]...[]MyType, len(raw))
-            for i, raw := range raw {
-                // We need the &(*target)[i] because at each stage we want to
-                // keep target as a pointer.  (It only really has to be a
+            // the loop we have one less layer of slice on src and dst
+            *dst = make([][]...[]MyType, len(src))
+            for i, src := range src {
+                // We need the &(*dst)[i] because at each stage we want to
+                // keep dst as a pointer.  (It only really has to be a
                 // pointer at the innermost level, but it's easiest to be
                 // consistent.)
-                target := &(*target)[i]
-            
-                // (now we have `target *MyType` and `raw json.RawMessage`)
-                __unmarshalMyType(target, raw)
+                dst := &(*dst)[i]
+           
+                // (now we have `dst *MyType` and `src json.RawMessage`)
+                __unmarshalMyType(dst, src)
 
             } // (also n times)
 
          Note that if the field also uses a pointer (`[][]...[]*MyType`), we
          now pass around `*[][]...[]*MyType`; again in principle
          `[][]...[]*MyType` would work but require more special-casing.  Thus
-         in the innermost loop, `target` is of type `**MyType`, so we have to
-         pass `*target` to the unmarshal-helper.  Of course, since MyType is an
+         in the innermost loop, `dst` is of type `**MyType`, so we have to
+         pass `*dst` to the unmarshaler.  Of course, since MyType is an
          interface, I'm not sure why you'd any of that anyway.
 
          One additional trick is we wrap everything above in a block ({ ... }),
-         so that the variables target and raw may take on different types for
+         so that the variables dst and src may take on different types for
          each field we are handling, which would otherwise conflict.  (We could
          instead suffix the names, but that makes things much harder to read.)
     */}}
     {
-        target := &v.{{$field.GoName}}
-        raw := firstPass.{{$field.GoName}}
+        dst := &v.{{$field.GoName}}
+        src := firstPass.{{$field.GoName}}
         {{range $i := intRange $field.GoType.SliceDepth -}}
-        *target = make(
+        *dst = make(
             {{repeat (sub $field.GoType.SliceDepth $i) "[]"}}{{if $field.GoType.IsPointer}}*{{end}}{{$field.GoType.Unwrap.Reference}},
-            len(raw))
-        for i, raw := range raw {
-            target := &(*target)[i]
+            len(src))
+        for i, src := range src {
+            dst := &(*dst)[i]
         {{end -}}
         {{if $field.GoType.IsPointer -}}
         {{/* In this case, the parent for loop did `make([]*MyType, ...)` and
              we have a pointer into that list.  But we actually still need to
              initialize the *elements* of the list. */ -}}
-        *target = new({{$field.GoType.Unwrap.Reference}})
+        *dst = new({{$field.GoType.Unwrap.Reference}})
         {{end -}}
-        err = __unmarshal{{$field.GoType.Unwrap.Reference}}(
-            {{if $field.GoType.IsPointer}}*{{end}}target, raw)
+        err = {{$field.Unmarshaler $.Generator}}(
+            src, {{if $field.GoType.IsPointer}}*{{end}}dst)
         if err != nil {
             return fmt.Errorf(
                 "Unable to unmarshal {{$.GoName}}.{{$field.GoName}}: %w", err)
@@ -122,7 +118,8 @@ func (v *{{.GoName}}) UnmarshalJSON(b []byte) error {
         }
         {{end -}}
     }
-    {{end -}}{{/* end if .IsEmbedded + else if .IsAbstract */ -}}
+    {{end -}}{{/* end if/else .IsEmbedded */ -}}
+    {{end -}}{{/* end if .NeedsUnmarshaler */ -}}
     {{end}}{{/* end range .Fields */ -}}
 
     return nil
diff --git a/generate/unmarshal_helper.go.tmpl b/generate/unmarshal_helper.go.tmpl
index a2b721f..25d5346 100644
--- a/generate/unmarshal_helper.go.tmpl
+++ b/generate/unmarshal_helper.go.tmpl
@@ -1,15 +1,15 @@
 {{/* (the blank lines at the start are intentional, to separate
      the helper from the function it follows) */}}
 
-func __unmarshal{{.GoName}}(v *{{.GoName}}, m {{ref "encoding/json.RawMessage"}}) error {
-    if string(m) == "null" {
+func __unmarshal{{.GoName}}(b []byte, v *{{.GoName}}) error {
+    if string(b) == "null" {
         return nil
     }
 
     var tn struct {
         TypeName string `json:"__typename"`
     }
-    err := {{ref "encoding/json.Unmarshal"}}(m, &tn)
+    err := {{ref "encoding/json.Unmarshal"}}(b, &tn)
     if err != nil {
         return err
     }
@@ -18,7 +18,7 @@ func __unmarshal{{.GoName}}(v *{{.GoName}}, m {{ref "encoding/json.RawMessage"}}
     {{range .Implementations -}}
     case "{{.GraphQLName}}":
         *v = new({{.GoName}})
-        return {{ref "encoding/json.Unmarshal"}}(m, *v)
+        return {{ref "encoding/json.Unmarshal"}}(b, *v)
     {{end -}}
     case "":
         {{/* Likely if we're making a request to a mock server and the author
diff --git a/graphql/util.go b/graphql/util.go
index 9a399c3..5afa837 100644
--- a/graphql/util.go
+++ b/graphql/util.go
@@ -17,3 +17,18 @@ type NoUnmarshalJSON struct{}
 func (NoUnmarshalJSON) UnmarshalJSON(b []byte) error {
 	panic("NoUnmarshalJSON.UnmarshalJSON should never be called!")
 }
+
+// NoMarshalJSON is intended for the use of genqlient's generated code only.
+//
+// It is used to prevent a struct type from inheriting its embed's
+// MarshalJSON method, so if we construct a type:
+//	type T struct { E; NoMarshalJSON }
+// where E has an MarshalJSON method, T will not inherit it, per the Go
+// selector rules: https://golang.org/ref/spec#Selectors.
+type NoMarshalJSON struct{}
+
+// MarshalJSON should never be called; it exists only to prevent a sibling
+// MarshalJSON method from being promoted.
+func (NoMarshalJSON) MarshalJSON() ([]byte, error) {
+	panic("NoUnmarshalJSON.MarshalJSON should never be called!")
+}
diff --git a/internal/integration/generated.go b/internal/integration/generated.go
index c73aef5..40ab3a1 100644
--- a/internal/integration/generated.go
+++ b/internal/integration/generated.go
@@ -6,8 +6,10 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"time"
 
 	"github.com/Khan/genqlient/graphql"
+	"github.com/Khan/genqlient/internal/testutil"
 )
 
 // AnimalFields includes the GraphQL fields of Animal requested by the fragment AnimalFields.
@@ -19,6 +21,10 @@ type AnimalFields struct {
 
 func (v *AnimalFields) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*AnimalFields
 		Owner json.RawMessage `json:"owner"`
@@ -32,10 +38,10 @@ func (v *AnimalFields) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Owner
-		raw := firstPass.Owner
+		dst := &v.Owner
+		src := firstPass.Owner
 		err = __unmarshalAnimalFieldsOwnerBeing(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal AnimalFields.Owner: %w", err)
@@ -84,15 +90,15 @@ func (v *AnimalFieldsOwnerAnimal) GetTypename() string { return v.Typename }
 // GetId is a part of, and documented with, the interface AnimalFieldsOwnerBeing.
 func (v *AnimalFieldsOwnerAnimal) GetId() string { return v.Id }
 
-func __unmarshalAnimalFieldsOwnerBeing(v *AnimalFieldsOwnerBeing, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalAnimalFieldsOwnerBeing(b []byte, v *AnimalFieldsOwnerBeing) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -100,10 +106,10 @@ func __unmarshalAnimalFieldsOwnerBeing(v *AnimalFieldsOwnerBeing, m json.RawMess
 	switch tn.TypeName {
 	case "User":
 		*v = new(AnimalFieldsOwnerUser)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Animal":
 		*v = new(AnimalFieldsOwnerAnimal)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Being.__typename")
@@ -123,6 +129,10 @@ type AnimalFieldsOwnerUser struct {
 
 func (v *AnimalFieldsOwnerUser) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*AnimalFieldsOwnerUser
 		graphql.NoUnmarshalJSON
@@ -162,15 +172,15 @@ func (v *LuckyFieldsUser) implementsGraphQLInterfaceLuckyFields() {}
 // GetLuckyNumber is a part of, and documented with, the interface LuckyFields.
 func (v *LuckyFieldsUser) GetLuckyNumber() int { return v.LuckyNumber }
 
-func __unmarshalLuckyFields(v *LuckyFields, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalLuckyFields(b []byte, v *LuckyFields) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -178,7 +188,7 @@ func __unmarshalLuckyFields(v *LuckyFields, m json.RawMessage) error {
 	switch tn.TypeName {
 	case "User":
 		*v = new(LuckyFieldsUser)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Lucky.__typename")
@@ -196,6 +206,10 @@ type LuckyFieldsUser struct {
 
 func (v *LuckyFieldsUser) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*LuckyFieldsUser
 		graphql.NoUnmarshalJSON
@@ -242,6 +256,10 @@ type UserFields struct {
 
 func (v *UserFields) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*UserFields
 		graphql.NoUnmarshalJSON
@@ -266,6 +284,72 @@ func (v *UserFields) UnmarshalJSON(b []byte) error {
 	return nil
 }
 
+// __queryWithCustomMarshalInput is used internally by genqlient
+type __queryWithCustomMarshalInput struct {
+	Date time.Time `json:"-"`
+}
+
+func (v *__queryWithCustomMarshalInput) MarshalJSON() ([]byte, error) {
+
+	var fullObject struct {
+		*__queryWithCustomMarshalInput
+		Date json.RawMessage `json:"date"`
+		graphql.NoMarshalJSON
+	}
+	fullObject.__queryWithCustomMarshalInput = v
+
+	{
+
+		dst := &fullObject.Date
+		src := v.Date
+		var err error
+		*dst, err = testutil.MarshalDate(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"Unable to marshal __queryWithCustomMarshalInput.Date: %w", err)
+		}
+	}
+
+	return json.Marshal(&fullObject)
+}
+
+// __queryWithCustomMarshalSliceInput is used internally by genqlient
+type __queryWithCustomMarshalSliceInput struct {
+	Dates []time.Time `json:"-"`
+}
+
+func (v *__queryWithCustomMarshalSliceInput) MarshalJSON() ([]byte, error) {
+
+	var fullObject struct {
+		*__queryWithCustomMarshalSliceInput
+		Dates []json.RawMessage `json:"dates"`
+		graphql.NoMarshalJSON
+	}
+	fullObject.__queryWithCustomMarshalSliceInput = v
+
+	{
+
+		dst := &fullObject.Dates
+		src := v.Dates
+		*dst = make(
+			[]json.RawMessage,
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
+			var err error
+			*dst, err = testutil.MarshalDate(
+				&src)
+			if err != nil {
+				return nil, fmt.Errorf(
+					"Unable to marshal __queryWithCustomMarshalSliceInput.Dates: %w", err)
+			}
+		}
+	}
+
+	return json.Marshal(&fullObject)
+}
+
 // __queryWithFragmentsInput is used internally by genqlient
 type __queryWithFragmentsInput struct {
 	Ids []string `json:"ids"`
@@ -312,6 +396,92 @@ type failingQueryResponse struct {
 	Me   failingQueryMeUser `json:"me"`
 }
 
+// queryWithCustomMarshalResponse is returned by queryWithCustomMarshal on success.
+type queryWithCustomMarshalResponse struct {
+	UsersBornOn []queryWithCustomMarshalUsersBornOnUser `json:"usersBornOn"`
+}
+
+// queryWithCustomMarshalSliceResponse is returned by queryWithCustomMarshalSlice on success.
+type queryWithCustomMarshalSliceResponse struct {
+	UsersBornOnDates []queryWithCustomMarshalSliceUsersBornOnDatesUser `json:"usersBornOnDates"`
+}
+
+// queryWithCustomMarshalSliceUsersBornOnDatesUser includes the requested fields of the GraphQL type User.
+type queryWithCustomMarshalSliceUsersBornOnDatesUser struct {
+	Id        string    `json:"id"`
+	Name      string    `json:"name"`
+	Birthdate time.Time `json:"-"`
+}
+
+func (v *queryWithCustomMarshalSliceUsersBornOnDatesUser) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*queryWithCustomMarshalSliceUsersBornOnDatesUser
+		Birthdate json.RawMessage `json:"birthdate"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.queryWithCustomMarshalSliceUsersBornOnDatesUser = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		dst := &v.Birthdate
+		src := firstPass.Birthdate
+		err = testutil.UnmarshalDate(
+			src, dst)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal queryWithCustomMarshalSliceUsersBornOnDatesUser.Birthdate: %w", err)
+		}
+	}
+	return nil
+}
+
+// queryWithCustomMarshalUsersBornOnUser includes the requested fields of the GraphQL type User.
+type queryWithCustomMarshalUsersBornOnUser struct {
+	Id        string    `json:"id"`
+	Name      string    `json:"name"`
+	Birthdate time.Time `json:"-"`
+}
+
+func (v *queryWithCustomMarshalUsersBornOnUser) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*queryWithCustomMarshalUsersBornOnUser
+		Birthdate json.RawMessage `json:"birthdate"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.queryWithCustomMarshalUsersBornOnUser = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		dst := &v.Birthdate
+		src := firstPass.Birthdate
+		err = testutil.UnmarshalDate(
+			src, dst)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal queryWithCustomMarshalUsersBornOnUser.Birthdate: %w", err)
+		}
+	}
+	return nil
+}
+
 // queryWithFragmentsBeingsAnimal includes the requested fields of the GraphQL type Animal.
 type queryWithFragmentsBeingsAnimal struct {
 	Typename string                                       `json:"__typename"`
@@ -324,6 +494,10 @@ type queryWithFragmentsBeingsAnimal struct {
 
 func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*queryWithFragmentsBeingsAnimal
 		Owner json.RawMessage `json:"owner"`
@@ -337,10 +511,10 @@ func (v *queryWithFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Owner
-		raw := firstPass.Owner
+		dst := &v.Owner
+		src := firstPass.Owner
 		err = __unmarshalqueryWithFragmentsBeingsAnimalOwnerBeing(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal queryWithFragmentsBeingsAnimal.Owner: %w", err)
@@ -400,15 +574,15 @@ func (v *queryWithFragmentsBeingsAnimalOwnerAnimal) GetId() string { return v.Id
 // GetName is a part of, and documented with, the interface queryWithFragmentsBeingsAnimalOwnerBeing.
 func (v *queryWithFragmentsBeingsAnimalOwnerAnimal) GetName() string { return v.Name }
 
-func __unmarshalqueryWithFragmentsBeingsAnimalOwnerBeing(v *queryWithFragmentsBeingsAnimalOwnerBeing, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalqueryWithFragmentsBeingsAnimalOwnerBeing(b []byte, v *queryWithFragmentsBeingsAnimalOwnerBeing) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -416,10 +590,10 @@ func __unmarshalqueryWithFragmentsBeingsAnimalOwnerBeing(v *queryWithFragmentsBe
 	switch tn.TypeName {
 	case "User":
 		*v = new(queryWithFragmentsBeingsAnimalOwnerUser)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Animal":
 		*v = new(queryWithFragmentsBeingsAnimalOwnerAnimal)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Being.__typename")
@@ -474,15 +648,15 @@ func (v *queryWithFragmentsBeingsAnimal) GetId() string { return v.Id }
 // GetName is a part of, and documented with, the interface queryWithFragmentsBeingsBeing.
 func (v *queryWithFragmentsBeingsAnimal) GetName() string { return v.Name }
 
-func __unmarshalqueryWithFragmentsBeingsBeing(v *queryWithFragmentsBeingsBeing, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalqueryWithFragmentsBeingsBeing(b []byte, v *queryWithFragmentsBeingsBeing) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -490,10 +664,10 @@ func __unmarshalqueryWithFragmentsBeingsBeing(v *queryWithFragmentsBeingsBeing,
 	switch tn.TypeName {
 	case "User":
 		*v = new(queryWithFragmentsBeingsUser)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Animal":
 		*v = new(queryWithFragmentsBeingsAnimal)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Being.__typename")
@@ -524,6 +698,10 @@ type queryWithFragmentsResponse struct {
 
 func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*queryWithFragmentsResponse
 		Beings []json.RawMessage `json:"beings"`
@@ -537,15 +715,15 @@ func (v *queryWithFragmentsResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Beings
-		raw := firstPass.Beings
-		*target = make(
+		dst := &v.Beings
+		src := firstPass.Beings
+		*dst = make(
 			[]queryWithFragmentsBeingsBeing,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalqueryWithFragmentsBeingsBeing(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal queryWithFragmentsResponse.Beings: %w", err)
@@ -601,15 +779,15 @@ func (v *queryWithInterfaceListFieldBeingsAnimal) GetId() string { return v.Id }
 // GetName is a part of, and documented with, the interface queryWithInterfaceListFieldBeingsBeing.
 func (v *queryWithInterfaceListFieldBeingsAnimal) GetName() string { return v.Name }
 
-func __unmarshalqueryWithInterfaceListFieldBeingsBeing(v *queryWithInterfaceListFieldBeingsBeing, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalqueryWithInterfaceListFieldBeingsBeing(b []byte, v *queryWithInterfaceListFieldBeingsBeing) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -617,10 +795,10 @@ func __unmarshalqueryWithInterfaceListFieldBeingsBeing(v *queryWithInterfaceList
 	switch tn.TypeName {
 	case "User":
 		*v = new(queryWithInterfaceListFieldBeingsUser)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Animal":
 		*v = new(queryWithInterfaceListFieldBeingsAnimal)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Being.__typename")
@@ -644,6 +822,10 @@ type queryWithInterfaceListFieldResponse struct {
 
 func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*queryWithInterfaceListFieldResponse
 		Beings []json.RawMessage `json:"beings"`
@@ -657,15 +839,15 @@ func (v *queryWithInterfaceListFieldResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Beings
-		raw := firstPass.Beings
-		*target = make(
+		dst := &v.Beings
+		src := firstPass.Beings
+		*dst = make(
 			[]queryWithInterfaceListFieldBeingsBeing,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalqueryWithInterfaceListFieldBeingsBeing(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal queryWithInterfaceListFieldResponse.Beings: %w", err)
@@ -721,15 +903,15 @@ func (v *queryWithInterfaceListPointerFieldBeingsAnimal) GetId() string { return
 // GetName is a part of, and documented with, the interface queryWithInterfaceListPointerFieldBeingsBeing.
 func (v *queryWithInterfaceListPointerFieldBeingsAnimal) GetName() string { return v.Name }
 
-func __unmarshalqueryWithInterfaceListPointerFieldBeingsBeing(v *queryWithInterfaceListPointerFieldBeingsBeing, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalqueryWithInterfaceListPointerFieldBeingsBeing(b []byte, v *queryWithInterfaceListPointerFieldBeingsBeing) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -737,10 +919,10 @@ func __unmarshalqueryWithInterfaceListPointerFieldBeingsBeing(v *queryWithInterf
 	switch tn.TypeName {
 	case "User":
 		*v = new(queryWithInterfaceListPointerFieldBeingsUser)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Animal":
 		*v = new(queryWithInterfaceListPointerFieldBeingsAnimal)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Being.__typename")
@@ -764,6 +946,10 @@ type queryWithInterfaceListPointerFieldResponse struct {
 
 func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*queryWithInterfaceListPointerFieldResponse
 		Beings []json.RawMessage `json:"beings"`
@@ -777,16 +963,16 @@ func (v *queryWithInterfaceListPointerFieldResponse) UnmarshalJSON(b []byte) err
 	}
 
 	{
-		target := &v.Beings
-		raw := firstPass.Beings
-		*target = make(
+		dst := &v.Beings
+		src := firstPass.Beings
+		*dst = make(
 			[]*queryWithInterfaceListPointerFieldBeingsBeing,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
-			*target = new(queryWithInterfaceListPointerFieldBeingsBeing)
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
+			*dst = new(queryWithInterfaceListPointerFieldBeingsBeing)
 			err = __unmarshalqueryWithInterfaceListPointerFieldBeingsBeing(
-				*target, raw)
+				src, *dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal queryWithInterfaceListPointerFieldResponse.Beings: %w", err)
@@ -835,15 +1021,15 @@ func (v *queryWithInterfaceNoFragmentsBeingAnimal) GetId() string { return v.Id
 // GetName is a part of, and documented with, the interface queryWithInterfaceNoFragmentsBeing.
 func (v *queryWithInterfaceNoFragmentsBeingAnimal) GetName() string { return v.Name }
 
-func __unmarshalqueryWithInterfaceNoFragmentsBeing(v *queryWithInterfaceNoFragmentsBeing, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalqueryWithInterfaceNoFragmentsBeing(b []byte, v *queryWithInterfaceNoFragmentsBeing) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -851,10 +1037,10 @@ func __unmarshalqueryWithInterfaceNoFragmentsBeing(v *queryWithInterfaceNoFragme
 	switch tn.TypeName {
 	case "User":
 		*v = new(queryWithInterfaceNoFragmentsBeingUser)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Animal":
 		*v = new(queryWithInterfaceNoFragmentsBeingAnimal)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Being.__typename")
@@ -892,6 +1078,10 @@ type queryWithInterfaceNoFragmentsResponse struct {
 
 func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*queryWithInterfaceNoFragmentsResponse
 		Being json.RawMessage `json:"being"`
@@ -905,10 +1095,10 @@ func (v *queryWithInterfaceNoFragmentsResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Being
-		raw := firstPass.Being
+		dst := &v.Being
+		src := firstPass.Being
 		err = __unmarshalqueryWithInterfaceNoFragmentsBeing(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal queryWithInterfaceNoFragmentsResponse.Being: %w", err)
@@ -926,6 +1116,10 @@ type queryWithNamedFragmentsBeingsAnimal struct {
 
 func (v *queryWithNamedFragmentsBeingsAnimal) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*queryWithNamedFragmentsBeingsAnimal
 		graphql.NoUnmarshalJSON
@@ -976,15 +1170,15 @@ func (v *queryWithNamedFragmentsBeingsAnimal) GetTypename() string { return v.Ty
 // GetId is a part of, and documented with, the interface queryWithNamedFragmentsBeingsBeing.
 func (v *queryWithNamedFragmentsBeingsAnimal) GetId() string { return v.Id }
 
-func __unmarshalqueryWithNamedFragmentsBeingsBeing(v *queryWithNamedFragmentsBeingsBeing, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalqueryWithNamedFragmentsBeingsBeing(b []byte, v *queryWithNamedFragmentsBeingsBeing) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -992,10 +1186,10 @@ func __unmarshalqueryWithNamedFragmentsBeingsBeing(v *queryWithNamedFragmentsBei
 	switch tn.TypeName {
 	case "User":
 		*v = new(queryWithNamedFragmentsBeingsUser)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Animal":
 		*v = new(queryWithNamedFragmentsBeingsAnimal)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Being.__typename")
@@ -1014,6 +1208,10 @@ type queryWithNamedFragmentsBeingsUser struct {
 
 func (v *queryWithNamedFragmentsBeingsUser) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*queryWithNamedFragmentsBeingsUser
 		graphql.NoUnmarshalJSON
@@ -1040,6 +1238,10 @@ type queryWithNamedFragmentsResponse struct {
 
 func (v *queryWithNamedFragmentsResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*queryWithNamedFragmentsResponse
 		Beings []json.RawMessage `json:"beings"`
@@ -1053,15 +1255,15 @@ func (v *queryWithNamedFragmentsResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Beings
-		raw := firstPass.Beings
-		*target = make(
+		dst := &v.Beings
+		src := firstPass.Beings
+		*dst = make(
 			[]queryWithNamedFragmentsBeingsBeing,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalqueryWithNamedFragmentsBeingsBeing(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal queryWithNamedFragmentsResponse.Beings: %w", err)
@@ -1214,6 +1416,64 @@ query queryWithOmitempty ($id: ID) {
 	return &retval, err
 }
 
+func queryWithCustomMarshal(
+	ctx context.Context,
+	client graphql.Client,
+	date time.Time,
+) (*queryWithCustomMarshalResponse, error) {
+	__input := __queryWithCustomMarshalInput{
+		Date: date,
+	}
+	var err error
+
+	var retval queryWithCustomMarshalResponse
+	err = client.MakeRequest(
+		ctx,
+		"queryWithCustomMarshal",
+		`
+query queryWithCustomMarshal ($date: Date!) {
+	usersBornOn(date: $date) {
+		id
+		name
+		birthdate
+	}
+}
+`,
+		&retval,
+		&__input,
+	)
+	return &retval, err
+}
+
+func queryWithCustomMarshalSlice(
+	ctx context.Context,
+	client graphql.Client,
+	dates []time.Time,
+) (*queryWithCustomMarshalSliceResponse, error) {
+	__input := __queryWithCustomMarshalSliceInput{
+		Dates: dates,
+	}
+	var err error
+
+	var retval queryWithCustomMarshalSliceResponse
+	err = client.MakeRequest(
+		ctx,
+		"queryWithCustomMarshalSlice",
+		`
+query queryWithCustomMarshalSlice ($dates: [Date!]!) {
+	usersBornOnDates(dates: $dates) {
+		id
+		name
+		birthdate
+	}
+}
+`,
+		&retval,
+		&__input,
+	)
+	return &retval, err
+}
+
 func queryWithInterfaceNoFragments(
 	ctx context.Context,
 	client graphql.Client,
diff --git a/internal/integration/genqlient.yaml b/internal/integration/genqlient.yaml
index be42214..0fd9fb6 100644
--- a/internal/integration/genqlient.yaml
+++ b/internal/integration/genqlient.yaml
@@ -3,3 +3,8 @@ operations:
 - "*_test.go"
 generated: generated.go
 allow_broken_features: true
+bindings:
+  Date:
+    type: time.Time
+    marshaler: "github.com/Khan/genqlient/internal/testutil.MarshalDate"
+    unmarshaler: "github.com/Khan/genqlient/internal/testutil.UnmarshalDate"
diff --git a/internal/integration/schema.graphql b/internal/integration/schema.graphql
index badf85d..e7c5942 100644
--- a/internal/integration/schema.graphql
+++ b/internal/integration/schema.graphql
@@ -1,9 +1,13 @@
+scalar Date
+
 type Query {
   me: User
   user(id: ID): User
   being(id: ID!): Being
   beings(ids: [ID!]!): [Being]!
   lotteryWinner(number: Int!): Lucky
+  usersBornOn(date: Date!): [User!]!
+  usersBornOnDates(dates: [Date!]!): [User!]!
   fail: Boolean
 }
 
@@ -12,6 +16,7 @@ type User implements Being & Lucky {
   name: String!
   luckyNumber: Int
   hair: Hair
+  birthdate: Date
 }
 
 type Hair { color: String }   # silly name to confuse the name-generator
diff --git a/internal/integration/server/gqlgen_exec.go b/internal/integration/server/gqlgen_exec.go
index 98a2ba5..7c7e8e3 100644
--- a/internal/integration/server/gqlgen_exec.go
+++ b/internal/integration/server/gqlgen_exec.go
@@ -59,15 +59,18 @@ type ComplexityRoot struct {
 	}
 
 	Query struct {
-		Being         func(childComplexity int, id string) int
-		Beings        func(childComplexity int, ids []string) int
-		Fail          func(childComplexity int) int
-		LotteryWinner func(childComplexity int, number int) int
-		Me            func(childComplexity int) int
-		User          func(childComplexity int, id *string) int
+		Being            func(childComplexity int, id string) int
+		Beings           func(childComplexity int, ids []string) int
+		Fail             func(childComplexity int) int
+		LotteryWinner    func(childComplexity int, number int) int
+		Me               func(childComplexity int) int
+		User             func(childComplexity int, id *string) int
+		UsersBornOn      func(childComplexity int, date string) int
+		UsersBornOnDates func(childComplexity int, dates []string) int
 	}
 
 	User struct {
+		Birthdate   func(childComplexity int) int
 		Hair        func(childComplexity int) int
 		ID          func(childComplexity int) int
 		LuckyNumber func(childComplexity int) int
@@ -81,6 +84,8 @@ type QueryResolver interface {
 	Being(ctx context.Context, id string) (Being, error)
 	Beings(ctx context.Context, ids []string) ([]Being, error)
 	LotteryWinner(ctx context.Context, number int) (Lucky, error)
+	UsersBornOn(ctx context.Context, date string) ([]*User, error)
+	UsersBornOnDates(ctx context.Context, dates []string) ([]*User, error)
 	Fail(ctx context.Context) (*bool, error)
 }
 
@@ -210,6 +215,37 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
 
 		return e.complexity.Query.User(childComplexity, args["id"].(*string)), true
 
+	case "Query.usersBornOn":
+		if e.complexity.Query.UsersBornOn == nil {
+			break
+		}
+
+		args, err := ec.field_Query_usersBornOn_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Query.UsersBornOn(childComplexity, args["date"].(string)), true
+
+	case "Query.usersBornOnDates":
+		if e.complexity.Query.UsersBornOnDates == nil {
+			break
+		}
+
+		args, err := ec.field_Query_usersBornOnDates_args(context.TODO(), rawArgs)
+		if err != nil {
+			return 0, false
+		}
+
+		return e.complexity.Query.UsersBornOnDates(childComplexity, args["dates"].([]string)), true
+
+	case "User.birthdate":
+		if e.complexity.User.Birthdate == nil {
+			break
+		}
+
+		return e.complexity.User.Birthdate(childComplexity), true
+
 	case "User.hair":
 		if e.complexity.User.Hair == nil {
 			break
@@ -288,12 +324,16 @@ func (ec *executionContext) introspectType(name string) (*introspection.Type, er
 }
 
 var sources = []*ast.Source{
-	{Name: "../schema.graphql", Input: `type Query {
+	{Name: "../schema.graphql", Input: `scalar Date
+
+type Query {
   me: User
   user(id: ID): User
   being(id: ID!): Being
   beings(ids: [ID!]!): [Being]!
   lotteryWinner(number: Int!): Lucky
+  usersBornOn(date: Date!): [User!]!
+  usersBornOnDates(dates: [Date!]!): [User!]!
   fail: Boolean
 }
 
@@ -302,6 +342,7 @@ type User implements Being & Lucky {
   name: String!
   luckyNumber: Int
   hair: Hair
+  birthdate: Date
 }
 
 type Hair { color: String }   # silly name to confuse the name-generator
@@ -412,6 +453,36 @@ func (ec *executionContext) field_Query_user_args(ctx context.Context, rawArgs m
 	return args, nil
 }
 
+func (ec *executionContext) field_Query_usersBornOnDates_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 []string
+	if tmp, ok := rawArgs["dates"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("dates"))
+		arg0, err = ec.unmarshalNDate2ᚕstringᚄ(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["dates"] = arg0
+	return args, nil
+}
+
+func (ec *executionContext) field_Query_usersBornOn_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
+	var err error
+	args := map[string]interface{}{}
+	var arg0 string
+	if tmp, ok := rawArgs["date"]; ok {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("date"))
+		arg0, err = ec.unmarshalNDate2string(ctx, tmp)
+		if err != nil {
+			return nil, err
+		}
+	}
+	args["date"] = arg0
+	return args, nil
+}
+
 func (ec *executionContext) field___Type_enumValues_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
 	var err error
 	args := map[string]interface{}{}
@@ -877,6 +948,90 @@ func (ec *executionContext) _Query_lotteryWinner(ctx context.Context, field grap
 	return ec.marshalOLucky2githubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐLucky(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _Query_usersBornOn(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "Query",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   true,
+		IsResolver: true,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Query_usersBornOn_args(ctx, rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	fc.Args = args
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Query().UsersBornOn(rctx, args["date"].(string))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]*User)
+	fc.Result = res
+	return ec.marshalNUser2ᚕᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUserᚄ(ctx, field.Selections, res)
+}
+
+func (ec *executionContext) _Query_usersBornOnDates(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "Query",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   true,
+		IsResolver: true,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	rawArgs := field.ArgumentMap(ec.Variables)
+	args, err := ec.field_Query_usersBornOnDates_args(ctx, rawArgs)
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	fc.Args = args
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return ec.resolvers.Query().UsersBornOnDates(rctx, args["dates"].([]string))
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		if !graphql.HasFieldError(ctx, fc) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	res := resTmp.([]*User)
+	fc.Result = res
+	return ec.marshalNUser2ᚕᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUserᚄ(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) _Query_fail(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -1114,6 +1269,38 @@ func (ec *executionContext) _User_hair(ctx context.Context, field graphql.Collec
 	return ec.marshalOHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐHair(ctx, field.Selections, res)
 }
 
+func (ec *executionContext) _User_birthdate(ctx context.Context, field graphql.CollectedField, obj *User) (ret graphql.Marshaler) {
+	defer func() {
+		if r := recover(); r != nil {
+			ec.Error(ctx, ec.Recover(ctx, r))
+			ret = graphql.Null
+		}
+	}()
+	fc := &graphql.FieldContext{
+		Object:     "User",
+		Field:      field,
+		Args:       nil,
+		IsMethod:   false,
+		IsResolver: false,
+	}
+
+	ctx = graphql.WithFieldContext(ctx, fc)
+	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
+		ctx = rctx // use context from middleware stack in children
+		return obj.Birthdate, nil
+	})
+	if err != nil {
+		ec.Error(ctx, err)
+		return graphql.Null
+	}
+	if resTmp == nil {
+		return graphql.Null
+	}
+	res := resTmp.(*string)
+	fc.Result = res
+	return ec.marshalODate2ᚖstring(ctx, field.Selections, res)
+}
+
 func (ec *executionContext) ___Directive_name(ctx context.Context, field graphql.CollectedField, obj *introspection.Directive) (ret graphql.Marshaler) {
 	defer func() {
 		if r := recover(); r != nil {
@@ -2413,6 +2600,34 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
 				res = ec._Query_lotteryWinner(ctx, field)
 				return res
 			})
+		case "usersBornOn":
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Query_usersBornOn(ctx, field)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
+		case "usersBornOnDates":
+			field := field
+			out.Concurrently(i, func() (res graphql.Marshaler) {
+				defer func() {
+					if r := recover(); r != nil {
+						ec.Error(ctx, ec.Recover(ctx, r))
+					}
+				}()
+				res = ec._Query_usersBornOnDates(ctx, field)
+				if res == graphql.Null {
+					atomic.AddUint32(&invalids, 1)
+				}
+				return res
+			})
 		case "fail":
 			field := field
 			out.Concurrently(i, func() (res graphql.Marshaler) {
@@ -2464,6 +2679,8 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj
 			out.Values[i] = ec._User_luckyNumber(ctx, field, obj)
 		case "hair":
 			out.Values[i] = ec._User_hair(ctx, field, obj)
+		case "birthdate":
+			out.Values[i] = ec._User_birthdate(ctx, field, obj)
 		default:
 			panic("unknown field " + strconv.Quote(field.Name))
 		}
@@ -2772,6 +2989,51 @@ func (ec *executionContext) marshalNBoolean2bool(ctx context.Context, sel ast.Se
 	return res
 }
 
+func (ec *executionContext) unmarshalNDate2string(ctx context.Context, v interface{}) (string, error) {
+	res, err := graphql.UnmarshalString(v)
+	return res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalNDate2string(ctx context.Context, sel ast.SelectionSet, v string) graphql.Marshaler {
+	res := graphql.MarshalString(v)
+	if res == graphql.Null {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "must not be null")
+		}
+	}
+	return res
+}
+
+func (ec *executionContext) unmarshalNDate2ᚕstringᚄ(ctx context.Context, v interface{}) ([]string, error) {
+	var vSlice []interface{}
+	if v != nil {
+		if tmp1, ok := v.([]interface{}); ok {
+			vSlice = tmp1
+		} else {
+			vSlice = []interface{}{v}
+		}
+	}
+	var err error
+	res := make([]string, len(vSlice))
+	for i := range vSlice {
+		ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i))
+		res[i], err = ec.unmarshalNDate2string(ctx, vSlice[i])
+		if err != nil {
+			return nil, err
+		}
+	}
+	return res, nil
+}
+
+func (ec *executionContext) marshalNDate2ᚕstringᚄ(ctx context.Context, sel ast.SelectionSet, v []string) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	for i := range v {
+		ret[i] = ec.marshalNDate2string(ctx, sel, v[i])
+	}
+
+	return ret
+}
+
 func (ec *executionContext) unmarshalNID2string(ctx context.Context, v interface{}) (string, error) {
 	res, err := graphql.UnmarshalID(v)
 	return res, graphql.ErrorOnPath(ctx, err)
@@ -2857,6 +3119,53 @@ func (ec *executionContext) marshalNString2string(ctx context.Context, sel ast.S
 	return res
 }
 
+func (ec *executionContext) marshalNUser2ᚕᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUserᚄ(ctx context.Context, sel ast.SelectionSet, v []*User) graphql.Marshaler {
+	ret := make(graphql.Array, len(v))
+	var wg sync.WaitGroup
+	isLen1 := len(v) == 1
+	if !isLen1 {
+		wg.Add(len(v))
+	}
+	for i := range v {
+		i := i
+		fc := &graphql.FieldContext{
+			Index:  &i,
+			Result: &v[i],
+		}
+		ctx := graphql.WithFieldContext(ctx, fc)
+		f := func(i int) {
+			defer func() {
+				if r := recover(); r != nil {
+					ec.Error(ctx, ec.Recover(ctx, r))
+					ret = nil
+				}
+			}()
+			if !isLen1 {
+				defer wg.Done()
+			}
+			ret[i] = ec.marshalNUser2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUser(ctx, sel, v[i])
+		}
+		if isLen1 {
+			f(i)
+		} else {
+			go f(i)
+		}
+
+	}
+	wg.Wait()
+	return ret
+}
+
+func (ec *executionContext) marshalNUser2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐUser(ctx context.Context, sel ast.SelectionSet, v *User) graphql.Marshaler {
+	if v == nil {
+		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
+			ec.Errorf(ctx, "must not be null")
+		}
+		return graphql.Null
+	}
+	return ec._User(ctx, sel, v)
+}
+
 func (ec *executionContext) marshalN__Directive2githubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐDirective(ctx context.Context, sel ast.SelectionSet, v introspection.Directive) graphql.Marshaler {
 	return ec.___Directive(ctx, sel, &v)
 }
@@ -3124,6 +3433,21 @@ func (ec *executionContext) marshalOBoolean2ᚖbool(ctx context.Context, sel ast
 	return graphql.MarshalBoolean(*v)
 }
 
+func (ec *executionContext) unmarshalODate2ᚖstring(ctx context.Context, v interface{}) (*string, error) {
+	if v == nil {
+		return nil, nil
+	}
+	res, err := graphql.UnmarshalString(v)
+	return &res, graphql.ErrorOnPath(ctx, err)
+}
+
+func (ec *executionContext) marshalODate2ᚖstring(ctx context.Context, sel ast.SelectionSet, v *string) graphql.Marshaler {
+	if v == nil {
+		return graphql.Null
+	}
+	return graphql.MarshalString(*v)
+}
+
 func (ec *executionContext) marshalOHair2ᚖgithubᚗcomᚋKhanᚋgenqlientᚋinternalᚋintegrationᚋserverᚐHair(ctx context.Context, sel ast.SelectionSet, v *Hair) graphql.Marshaler {
 	if v == nil {
 		return graphql.Null
diff --git a/internal/integration/server/gqlgen_models.go b/internal/integration/server/gqlgen_models.go
index a8b225b..b74a709 100644
--- a/internal/integration/server/gqlgen_models.go
+++ b/internal/integration/server/gqlgen_models.go
@@ -35,10 +35,11 @@ type Hair struct {
 }
 
 type User struct {
-	ID          string `json:"id"`
-	Name        string `json:"name"`
-	LuckyNumber *int   `json:"luckyNumber"`
-	Hair        *Hair  `json:"hair"`
+	ID          string  `json:"id"`
+	Name        string  `json:"name"`
+	LuckyNumber *int    `json:"luckyNumber"`
+	Hair        *Hair   `json:"hair"`
+	Birthdate   *string `json:"birthdate"`
 }
 
 func (User) IsBeing() {}
diff --git a/internal/integration/server/server.go b/internal/integration/server/server.go
index bbf0a29..3883504 100644
--- a/internal/integration/server/server.go
+++ b/internal/integration/server/server.go
@@ -15,7 +15,8 @@ func intptr(v int) *int       { return &v }
 var users = []*User{
 	{
 		ID: "1", Name: "Yours Truly", LuckyNumber: intptr(17),
-		Hair: &Hair{Color: strptr("Black")},
+		Birthdate: strptr("2025-01-01"),
+		Hair:      &Hair{Color: strptr("Black")},
 	},
 	{ID: "2", Name: "Raven", LuckyNumber: intptr(-1), Hair: nil},
 }
@@ -40,6 +41,18 @@ func userByID(id string) *User {
 	return nil
 }
 
+func usersByBirthdates(dates []string) []*User {
+	var retval []*User
+	for _, date := range dates {
+		for _, user := range users {
+			if user.Birthdate != nil && *user.Birthdate == date {
+				retval = append(retval, user)
+			}
+		}
+	}
+	return retval
+}
+
 func beingByID(id string) Being {
 	for _, user := range users {
 		if id == user.ID {
@@ -86,6 +99,14 @@ func (r *queryResolver) LotteryWinner(ctx context.Context, number int) (Lucky, e
 	return nil, nil
 }
 
+func (r *queryResolver) UsersBornOn(ctx context.Context, date string) ([]*User, error) {
+	return usersByBirthdates([]string{date}), nil
+}
+
+func (r *queryResolver) UsersBornOnDates(ctx context.Context, dates []string) ([]*User, error) {
+	return usersByBirthdates(dates), nil
+}
+
 func (r *queryResolver) Fail(ctx context.Context) (*bool, error) {
 	f := true
 	return &f, fmt.Errorf("oh no")


Test patch:
diff --git a/generate/generate_test.go b/generate/generate_test.go
index 3612598..fcc05b5 100644
--- a/generate/generate_test.go
+++ b/generate/generate_test.go
@@ -83,8 +83,13 @@ func TestGenerate(t *testing.T) {
 				ExportOperations: queriesFilename,
 				ContextType:      "-",
 				Bindings: map[string]*TypeBinding{
-					"ID":          {Type: "github.com/Khan/genqlient/internal/testutil.ID"},
-					"DateTime":    {Type: "time.Time"},
+					"ID":       {Type: "github.com/Khan/genqlient/internal/testutil.ID"},
+					"DateTime": {Type: "time.Time"},
+					"Date": {
+						Type:        "time.Time",
+						Marshaler:   "github.com/Khan/genqlient/internal/testutil.MarshalDate",
+						Unmarshaler: "github.com/Khan/genqlient/internal/testutil.UnmarshalDate",
+					},
 					"Junk":        {Type: "interface{}"},
 					"ComplexJunk": {Type: "[]map[string]*[]*map[string]interface{}"},
 					"Pokemon": {
diff --git a/generate/testdata/queries/CustomMarshal.graphql b/generate/testdata/queries/CustomMarshal.graphql
new file mode 100644
index 0000000..0a009ff
--- /dev/null
+++ b/generate/testdata/queries/CustomMarshal.graphql
@@ -0,0 +1,6 @@
+query CustomMarshal($date: Date!) {
+  usersBornOn(date: $date) {
+    id
+    birthdate
+  }
+}
diff --git a/generate/testdata/queries/CustomMarshalSlice.graphql b/generate/testdata/queries/CustomMarshalSlice.graphql
new file mode 100644
index 0000000..0ec8a8b
--- /dev/null
+++ b/generate/testdata/queries/CustomMarshalSlice.graphql
@@ -0,0 +1,8 @@
+query CustomMarshalSlice(
+  $datesss: [[[Date!]!]!]!,
+  # @genqlient(pointer: true)
+  $datesssp: [[[Date!]!]!]!,
+) {
+  acceptsListOfListOfListsOfDates(datesss: $datesss)
+  withPointer: acceptsListOfListOfListsOfDates(datesss: $datesssp)
+}
diff --git a/generate/testdata/queries/schema.graphql b/generate/testdata/queries/schema.graphql
index 3392352..d0e6908 100644
--- a/generate/testdata/queries/schema.graphql
+++ b/generate/testdata/queries/schema.graphql
@@ -3,6 +3,7 @@
 We don't really have anything useful to do with this description though.
 """
 scalar DateTime
+scalar Date
 scalar Junk
 scalar ComplexJunk
 
@@ -44,6 +45,7 @@ input UserQueryInput {
   role: Role
   names: [String]
   hasPokemon: PokemonInput
+  birthdate: Date
 }
 
 type AuthMethod {
@@ -66,6 +68,7 @@ type User {
   authMethods: [AuthMethod!]!
   pokemon: [Pokemon!]
   greeting: Clip
+  birthdate: Date
 }
 
 """An audio clip, such as of a user saying hello."""
@@ -153,6 +156,9 @@ type Query {
 
   """usersWithRole looks a user up by role."""
   usersWithRole(role: Role!): [User!]!
+
+  usersBornOn(date: Date!): [User!]!
+
   root: Topic!
   randomItem: Content!
   randomLeaf: LeafContent!
@@ -163,6 +169,7 @@ type Query {
   listOfListsOfLists: [[[String!]!]!]!
   listOfListsOfListsOfContent: [[[Content!]!]!]!
   recur(input: RecursiveInput!): Recursive
+  acceptsListOfListOfListsOfDates(datesss: [[[Date!]!]!]!): Boolean
 }
 
 type Mutation {
diff --git a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go
index 584d6d7..a5df875 100644
--- a/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-ComplexInlineFragments.graphql-ComplexInlineFragments.graphql.go
@@ -55,15 +55,15 @@ func (v *ComplexInlineFragmentsConflictingStuffTopic) implementsGraphQLInterface
 // GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsConflictingStuffContent.
 func (v *ComplexInlineFragmentsConflictingStuffTopic) GetTypename() string { return v.Typename }
 
-func __unmarshalComplexInlineFragmentsConflictingStuffContent(v *ComplexInlineFragmentsConflictingStuffContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalComplexInlineFragmentsConflictingStuffContent(b []byte, v *ComplexInlineFragmentsConflictingStuffContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -71,13 +71,13 @@ func __unmarshalComplexInlineFragmentsConflictingStuffContent(v *ComplexInlineFr
 	switch tn.TypeName {
 	case "Article":
 		*v = new(ComplexInlineFragmentsConflictingStuffArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(ComplexInlineFragmentsConflictingStuffVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(ComplexInlineFragmentsConflictingStuffTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -142,15 +142,15 @@ func (v *ComplexInlineFragmentsNestedStuffTopic) implementsGraphQLInterfaceCompl
 // GetTypename is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffContent.
 func (v *ComplexInlineFragmentsNestedStuffTopic) GetTypename() string { return v.Typename }
 
-func __unmarshalComplexInlineFragmentsNestedStuffContent(v *ComplexInlineFragmentsNestedStuffContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalComplexInlineFragmentsNestedStuffContent(b []byte, v *ComplexInlineFragmentsNestedStuffContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -158,13 +158,13 @@ func __unmarshalComplexInlineFragmentsNestedStuffContent(v *ComplexInlineFragmen
 	switch tn.TypeName {
 	case "Article":
 		*v = new(ComplexInlineFragmentsNestedStuffArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(ComplexInlineFragmentsNestedStuffVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(ComplexInlineFragmentsNestedStuffTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -182,6 +182,10 @@ type ComplexInlineFragmentsNestedStuffTopic struct {
 
 func (v *ComplexInlineFragmentsNestedStuffTopic) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*ComplexInlineFragmentsNestedStuffTopic
 		Children []json.RawMessage `json:"children"`
@@ -195,15 +199,15 @@ func (v *ComplexInlineFragmentsNestedStuffTopic) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Children
-		raw := firstPass.Children
-		*target = make(
+		dst := &v.Children
+		src := firstPass.Children
+		*dst = make(
 			[]ComplexInlineFragmentsNestedStuffTopicChildrenContent,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalComplexInlineFragmentsNestedStuffTopicChildrenContent(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal ComplexInlineFragmentsNestedStuffTopic.Children: %w", err)
@@ -229,6 +233,10 @@ type ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTop
 
 func (v *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic
 		Children []json.RawMessage `json:"children"`
@@ -242,15 +250,15 @@ func (v *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParen
 	}
 
 	{
-		target := &v.Children
-		raw := firstPass.Children
-		*target = make(
+		dst := &v.Children
+		src := firstPass.Children
+		*dst = make(
 			[]ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicChildrenContent,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicChildrenContent(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopic.Children: %w", err)
@@ -344,15 +352,15 @@ func (v *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParen
 	return v.Name
 }
 
-func __unmarshalComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicChildrenContent(v *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicChildrenContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicChildrenContent(b []byte, v *ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicChildrenContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -360,13 +368,13 @@ func __unmarshalComplexInlineFragmentsNestedStuffTopicChildrenArticleParentConte
 	switch tn.TypeName {
 	case "Article":
 		*v = new(ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicChildrenArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicChildrenVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(ComplexInlineFragmentsNestedStuffTopicChildrenArticleParentContentParentTopicChildrenTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -447,15 +455,15 @@ func (v *ComplexInlineFragmentsNestedStuffTopicChildrenTopic) GetTypename() stri
 // GetId is a part of, and documented with, the interface ComplexInlineFragmentsNestedStuffTopicChildrenContent.
 func (v *ComplexInlineFragmentsNestedStuffTopicChildrenTopic) GetId() testutil.ID { return v.Id }
 
-func __unmarshalComplexInlineFragmentsNestedStuffTopicChildrenContent(v *ComplexInlineFragmentsNestedStuffTopicChildrenContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalComplexInlineFragmentsNestedStuffTopicChildrenContent(b []byte, v *ComplexInlineFragmentsNestedStuffTopicChildrenContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -463,13 +471,13 @@ func __unmarshalComplexInlineFragmentsNestedStuffTopicChildrenContent(v *Complex
 	switch tn.TypeName {
 	case "Article":
 		*v = new(ComplexInlineFragmentsNestedStuffTopicChildrenArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(ComplexInlineFragmentsNestedStuffTopicChildrenVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(ComplexInlineFragmentsNestedStuffTopicChildrenTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -565,15 +573,15 @@ func (v *ComplexInlineFragmentsRandomItemTopic) GetId() testutil.ID { return v.I
 // GetName is a part of, and documented with, the interface ComplexInlineFragmentsRandomItemContent.
 func (v *ComplexInlineFragmentsRandomItemTopic) GetName() string { return v.Name }
 
-func __unmarshalComplexInlineFragmentsRandomItemContent(v *ComplexInlineFragmentsRandomItemContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalComplexInlineFragmentsRandomItemContent(b []byte, v *ComplexInlineFragmentsRandomItemContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -581,13 +589,13 @@ func __unmarshalComplexInlineFragmentsRandomItemContent(v *ComplexInlineFragment
 	switch tn.TypeName {
 	case "Article":
 		*v = new(ComplexInlineFragmentsRandomItemArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(ComplexInlineFragmentsRandomItemVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(ComplexInlineFragmentsRandomItemTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -721,15 +729,15 @@ func (v *ComplexInlineFragmentsRepeatedStuffTopic) GetName() string { return v.N
 // GetOtherName is a part of, and documented with, the interface ComplexInlineFragmentsRepeatedStuffContent.
 func (v *ComplexInlineFragmentsRepeatedStuffTopic) GetOtherName() string { return v.OtherName }
 
-func __unmarshalComplexInlineFragmentsRepeatedStuffContent(v *ComplexInlineFragmentsRepeatedStuffContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalComplexInlineFragmentsRepeatedStuffContent(b []byte, v *ComplexInlineFragmentsRepeatedStuffContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -737,13 +745,13 @@ func __unmarshalComplexInlineFragmentsRepeatedStuffContent(v *ComplexInlineFragm
 	switch tn.TypeName {
 	case "Article":
 		*v = new(ComplexInlineFragmentsRepeatedStuffArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(ComplexInlineFragmentsRepeatedStuffVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(ComplexInlineFragmentsRepeatedStuffTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -789,6 +797,10 @@ type ComplexInlineFragmentsResponse struct {
 
 func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*ComplexInlineFragmentsResponse
 		RandomItem       json.RawMessage `json:"randomItem"`
@@ -805,10 +817,10 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RandomItem
-		raw := firstPass.RandomItem
+		dst := &v.RandomItem
+		src := firstPass.RandomItem
 		err = __unmarshalComplexInlineFragmentsRandomItemContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal ComplexInlineFragmentsResponse.RandomItem: %w", err)
@@ -816,10 +828,10 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RepeatedStuff
-		raw := firstPass.RepeatedStuff
+		dst := &v.RepeatedStuff
+		src := firstPass.RepeatedStuff
 		err = __unmarshalComplexInlineFragmentsRepeatedStuffContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal ComplexInlineFragmentsResponse.RepeatedStuff: %w", err)
@@ -827,10 +839,10 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.ConflictingStuff
-		raw := firstPass.ConflictingStuff
+		dst := &v.ConflictingStuff
+		src := firstPass.ConflictingStuff
 		err = __unmarshalComplexInlineFragmentsConflictingStuffContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal ComplexInlineFragmentsResponse.ConflictingStuff: %w", err)
@@ -838,10 +850,10 @@ func (v *ComplexInlineFragmentsResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.NestedStuff
-		raw := firstPass.NestedStuff
+		dst := &v.NestedStuff
+		src := firstPass.NestedStuff
 		err = __unmarshalComplexInlineFragmentsNestedStuffContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal ComplexInlineFragmentsResponse.NestedStuff: %w", err)
diff --git a/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go
index 0d59e49..ecd1254 100644
--- a/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-ComplexNamedFragments.graphql-ComplexNamedFragments.graphql.go
@@ -17,6 +17,10 @@ type ComplexNamedFragmentsResponse struct {
 
 func (v *ComplexNamedFragmentsResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*ComplexNamedFragmentsResponse
 		graphql.NoUnmarshalJSON
@@ -77,15 +81,15 @@ func (v *ContentFieldsTopic) GetName() string { return v.Name }
 // GetUrl is a part of, and documented with, the interface ContentFields.
 func (v *ContentFieldsTopic) GetUrl() string { return v.Url }
 
-func __unmarshalContentFields(v *ContentFields, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalContentFields(b []byte, v *ContentFields) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -93,13 +97,13 @@ func __unmarshalContentFields(v *ContentFields, m json.RawMessage) error {
 	switch tn.TypeName {
 	case "Article":
 		*v = new(ContentFieldsArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(ContentFieldsVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(ContentFieldsTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -148,6 +152,10 @@ type InnerQueryFragment struct {
 
 func (v *InnerQueryFragment) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InnerQueryFragment
 		RandomItem json.RawMessage `json:"randomItem"`
@@ -163,10 +171,10 @@ func (v *InnerQueryFragment) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RandomItem
-		raw := firstPass.RandomItem
+		dst := &v.RandomItem
+		src := firstPass.RandomItem
 		err = __unmarshalInnerQueryFragmentRandomItemContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal InnerQueryFragment.RandomItem: %w", err)
@@ -174,10 +182,10 @@ func (v *InnerQueryFragment) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RandomLeaf
-		raw := firstPass.RandomLeaf
+		dst := &v.RandomLeaf
+		src := firstPass.RandomLeaf
 		err = __unmarshalInnerQueryFragmentRandomLeafLeafContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal InnerQueryFragment.RandomLeaf: %w", err)
@@ -185,10 +193,10 @@ func (v *InnerQueryFragment) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.OtherLeaf
-		raw := firstPass.OtherLeaf
+		dst := &v.OtherLeaf
+		src := firstPass.OtherLeaf
 		err = __unmarshalInnerQueryFragmentOtherLeafLeafContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal InnerQueryFragment.OtherLeaf: %w", err)
@@ -205,6 +213,10 @@ type InnerQueryFragmentOtherLeafArticle struct {
 
 func (v *InnerQueryFragmentOtherLeafArticle) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InnerQueryFragmentOtherLeafArticle
 		graphql.NoUnmarshalJSON
@@ -250,15 +262,15 @@ func (v *InnerQueryFragmentOtherLeafVideo) implementsGraphQLInterfaceInnerQueryF
 // GetTypename is a part of, and documented with, the interface InnerQueryFragmentOtherLeafLeafContent.
 func (v *InnerQueryFragmentOtherLeafVideo) GetTypename() string { return v.Typename }
 
-func __unmarshalInnerQueryFragmentOtherLeafLeafContent(v *InnerQueryFragmentOtherLeafLeafContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInnerQueryFragmentOtherLeafLeafContent(b []byte, v *InnerQueryFragmentOtherLeafLeafContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -266,10 +278,10 @@ func __unmarshalInnerQueryFragmentOtherLeafLeafContent(v *InnerQueryFragmentOthe
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InnerQueryFragmentOtherLeafArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InnerQueryFragmentOtherLeafVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing LeafContent.__typename")
@@ -288,6 +300,10 @@ type InnerQueryFragmentOtherLeafVideo struct {
 
 func (v *InnerQueryFragmentOtherLeafVideo) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InnerQueryFragmentOtherLeafVideo
 		graphql.NoUnmarshalJSON
@@ -323,6 +339,10 @@ type InnerQueryFragmentRandomItemArticle struct {
 
 func (v *InnerQueryFragmentRandomItemArticle) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InnerQueryFragmentRandomItemArticle
 		graphql.NoUnmarshalJSON
@@ -401,15 +421,15 @@ func (v *InnerQueryFragmentRandomItemTopic) GetId() testutil.ID { return v.Id }
 // GetName is a part of, and documented with, the interface InnerQueryFragmentRandomItemContent.
 func (v *InnerQueryFragmentRandomItemTopic) GetName() string { return v.Name }
 
-func __unmarshalInnerQueryFragmentRandomItemContent(v *InnerQueryFragmentRandomItemContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInnerQueryFragmentRandomItemContent(b []byte, v *InnerQueryFragmentRandomItemContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -417,13 +437,13 @@ func __unmarshalInnerQueryFragmentRandomItemContent(v *InnerQueryFragmentRandomI
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InnerQueryFragmentRandomItemArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InnerQueryFragmentRandomItemVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(InnerQueryFragmentRandomItemTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -444,6 +464,10 @@ type InnerQueryFragmentRandomItemTopic struct {
 
 func (v *InnerQueryFragmentRandomItemTopic) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InnerQueryFragmentRandomItemTopic
 		graphql.NoUnmarshalJSON
@@ -475,6 +499,10 @@ type InnerQueryFragmentRandomItemVideo struct {
 
 func (v *InnerQueryFragmentRandomItemVideo) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InnerQueryFragmentRandomItemVideo
 		graphql.NoUnmarshalJSON
@@ -507,6 +535,10 @@ type InnerQueryFragmentRandomLeafArticle struct {
 
 func (v *InnerQueryFragmentRandomLeafArticle) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InnerQueryFragmentRandomLeafArticle
 		graphql.NoUnmarshalJSON
@@ -552,15 +584,15 @@ func (v *InnerQueryFragmentRandomLeafVideo) implementsGraphQLInterfaceInnerQuery
 // GetTypename is a part of, and documented with, the interface InnerQueryFragmentRandomLeafLeafContent.
 func (v *InnerQueryFragmentRandomLeafVideo) GetTypename() string { return v.Typename }
 
-func __unmarshalInnerQueryFragmentRandomLeafLeafContent(v *InnerQueryFragmentRandomLeafLeafContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInnerQueryFragmentRandomLeafLeafContent(b []byte, v *InnerQueryFragmentRandomLeafLeafContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -568,10 +600,10 @@ func __unmarshalInnerQueryFragmentRandomLeafLeafContent(v *InnerQueryFragmentRan
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InnerQueryFragmentRandomLeafArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InnerQueryFragmentRandomLeafVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing LeafContent.__typename")
@@ -591,6 +623,10 @@ type InnerQueryFragmentRandomLeafVideo struct {
 
 func (v *InnerQueryFragmentRandomLeafVideo) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InnerQueryFragmentRandomLeafVideo
 		graphql.NoUnmarshalJSON
@@ -637,6 +673,10 @@ type MoreVideoFieldsParentTopic struct {
 
 func (v *MoreVideoFieldsParentTopic) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*MoreVideoFieldsParentTopic
 		Children []json.RawMessage `json:"children"`
@@ -656,15 +696,15 @@ func (v *MoreVideoFieldsParentTopic) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Children
-		raw := firstPass.Children
-		*target = make(
+		dst := &v.Children
+		src := firstPass.Children
+		*dst = make(
 			[]MoreVideoFieldsParentTopicChildrenContent,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalMoreVideoFieldsParentTopicChildrenContent(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal MoreVideoFieldsParentTopic.Children: %w", err)
@@ -712,15 +752,15 @@ func (v *MoreVideoFieldsParentTopicChildrenTopic) implementsGraphQLInterfaceMore
 // GetTypename is a part of, and documented with, the interface MoreVideoFieldsParentTopicChildrenContent.
 func (v *MoreVideoFieldsParentTopicChildrenTopic) GetTypename() *string { return v.Typename }
 
-func __unmarshalMoreVideoFieldsParentTopicChildrenContent(v *MoreVideoFieldsParentTopicChildrenContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalMoreVideoFieldsParentTopicChildrenContent(b []byte, v *MoreVideoFieldsParentTopicChildrenContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -728,13 +768,13 @@ func __unmarshalMoreVideoFieldsParentTopicChildrenContent(v *MoreVideoFieldsPare
 	switch tn.TypeName {
 	case "Article":
 		*v = new(MoreVideoFieldsParentTopicChildrenArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(MoreVideoFieldsParentTopicChildrenVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(MoreVideoFieldsParentTopicChildrenTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -757,6 +797,10 @@ type MoreVideoFieldsParentTopicChildrenVideo struct {
 
 func (v *MoreVideoFieldsParentTopicChildrenVideo) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*MoreVideoFieldsParentTopicChildrenVideo
 		graphql.NoUnmarshalJSON
@@ -786,6 +830,10 @@ type QueryFragment struct {
 
 func (v *QueryFragment) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*QueryFragment
 		graphql.NoUnmarshalJSON
@@ -818,6 +866,10 @@ type VideoFields struct {
 
 func (v *VideoFields) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*VideoFields
 		graphql.NoUnmarshalJSON
diff --git a/generate/testdata/snapshots/TestGenerate-CustomMarshal.graphql-CustomMarshal.graphql.go b/generate/testdata/snapshots/TestGenerate-CustomMarshal.graphql-CustomMarshal.graphql.go
new file mode 100644
index 0000000..00c711d
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-CustomMarshal.graphql-CustomMarshal.graphql.go
@@ -0,0 +1,118 @@
+package test
+
+// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/Khan/genqlient/graphql"
+	"github.com/Khan/genqlient/internal/testutil"
+)
+
+// CustomMarshalResponse is returned by CustomMarshal on success.
+type CustomMarshalResponse struct {
+	UsersBornOn []CustomMarshalUsersBornOnUser `json:"usersBornOn"`
+}
+
+// CustomMarshalUsersBornOnUser includes the requested fields of the GraphQL type User.
+// The GraphQL type's documentation follows.
+//
+// A User is a user!
+type CustomMarshalUsersBornOnUser struct {
+	// id is the user's ID.
+	//
+	// It is stable, unique, and opaque, like all good IDs.
+	Id        testutil.ID `json:"id"`
+	Birthdate time.Time   `json:"-"`
+}
+
+func (v *CustomMarshalUsersBornOnUser) UnmarshalJSON(b []byte) error {
+
+	if string(b) == "null" {
+		return nil
+	}
+
+	var firstPass struct {
+		*CustomMarshalUsersBornOnUser
+		Birthdate json.RawMessage `json:"birthdate"`
+		graphql.NoUnmarshalJSON
+	}
+	firstPass.CustomMarshalUsersBornOnUser = v
+
+	err := json.Unmarshal(b, &firstPass)
+	if err != nil {
+		return err
+	}
+
+	{
+		dst := &v.Birthdate
+		src := firstPass.Birthdate
+		err = testutil.UnmarshalDate(
+			src, dst)
+		if err != nil {
+			return fmt.Errorf(
+				"Unable to unmarshal CustomMarshalUsersBornOnUser.Birthdate: %w", err)
+		}
+	}
+	return nil
+}
+
+// __CustomMarshalInput is used internally by genqlient
+type __CustomMarshalInput struct {
+	Date time.Time `json:"-"`
+}
+
+func (v *__CustomMarshalInput) MarshalJSON() ([]byte, error) {
+
+	var fullObject struct {
+		*__CustomMarshalInput
+		Date json.RawMessage `json:"date"`
+		graphql.NoMarshalJSON
+	}
+	fullObject.__CustomMarshalInput = v
+
+	{
+
+		dst := &fullObject.Date
+		src := v.Date
+		var err error
+		*dst, err = testutil.MarshalDate(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"Unable to marshal __CustomMarshalInput.Date: %w", err)
+		}
+	}
+
+	return json.Marshal(&fullObject)
+}
+
+func CustomMarshal(
+	client graphql.Client,
+	date time.Time,
+) (*CustomMarshalResponse, error) {
+	__input := __CustomMarshalInput{
+		Date: date,
+	}
+	var err error
+
+	var retval CustomMarshalResponse
+	err = client.MakeRequest(
+		nil,
+		"CustomMarshal",
+		`
+query CustomMarshal ($date: Date!) {
+	usersBornOn(date: $date) {
+		id
+		birthdate
+	}
+}
+`,
+		&retval,
+		&__input,
+	)
+	return &retval, err
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-CustomMarshal.graphql-CustomMarshal.graphql.json b/generate/testdata/snapshots/TestGenerate-CustomMarshal.graphql-CustomMarshal.graphql.json
new file mode 100644
index 0000000..dd64162
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-CustomMarshal.graphql-CustomMarshal.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "CustomMarshal",
+      "query": "\nquery CustomMarshal ($date: Date!) {\n\tusersBornOn(date: $date) {\n\t\tid\n\t\tbirthdate\n\t}\n}\n",
+      "sourceLocation": "testdata/queries/CustomMarshal.graphql"
+    }
+  ]
+}
diff --git a/generate/testdata/snapshots/TestGenerate-CustomMarshalSlice.graphql-CustomMarshalSlice.graphql.go b/generate/testdata/snapshots/TestGenerate-CustomMarshalSlice.graphql-CustomMarshalSlice.graphql.go
new file mode 100644
index 0000000..d7462d3
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-CustomMarshalSlice.graphql-CustomMarshalSlice.graphql.go
@@ -0,0 +1,126 @@
+package test
+
+// Code generated by github.com/Khan/genqlient, DO NOT EDIT.
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/Khan/genqlient/graphql"
+	"github.com/Khan/genqlient/internal/testutil"
+)
+
+// CustomMarshalSliceResponse is returned by CustomMarshalSlice on success.
+type CustomMarshalSliceResponse struct {
+	AcceptsListOfListOfListsOfDates bool `json:"acceptsListOfListOfListsOfDates"`
+	WithPointer                     bool `json:"withPointer"`
+}
+
+// __CustomMarshalSliceInput is used internally by genqlient
+type __CustomMarshalSliceInput struct {
+	Datesss  [][][]time.Time  `json:"-"`
+	Datesssp [][][]*time.Time `json:"-"`
+}
+
+func (v *__CustomMarshalSliceInput) MarshalJSON() ([]byte, error) {
+
+	var fullObject struct {
+		*__CustomMarshalSliceInput
+		Datesss  [][][]json.RawMessage `json:"datesss"`
+		Datesssp [][][]json.RawMessage `json:"datesssp"`
+		graphql.NoMarshalJSON
+	}
+	fullObject.__CustomMarshalSliceInput = v
+
+	{
+
+		dst := &fullObject.Datesss
+		src := v.Datesss
+		*dst = make(
+			[][][]json.RawMessage,
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
+			*dst = make(
+				[][]json.RawMessage,
+				len(src))
+			for i, src := range src {
+				dst := &(*dst)[i]
+				*dst = make(
+					[]json.RawMessage,
+					len(src))
+				for i, src := range src {
+					dst := &(*dst)[i]
+					var err error
+					*dst, err = testutil.MarshalDate(
+						&src)
+					if err != nil {
+						return nil, fmt.Errorf(
+							"Unable to marshal __CustomMarshalSliceInput.Datesss: %w", err)
+					}
+				}
+			}
+		}
+	}
+	{
+
+		dst := &fullObject.Datesssp
+		src := v.Datesssp
+		*dst = make(
+			[][][]json.RawMessage,
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
+			*dst = make(
+				[][]json.RawMessage,
+				len(src))
+			for i, src := range src {
+				dst := &(*dst)[i]
+				*dst = make(
+					[]json.RawMessage,
+					len(src))
+				for i, src := range src {
+					dst := &(*dst)[i]
+					var err error
+					*dst, err = testutil.MarshalDate(
+						src)
+					if err != nil {
+						return nil, fmt.Errorf(
+							"Unable to marshal __CustomMarshalSliceInput.Datesssp: %w", err)
+					}
+				}
+			}
+		}
+	}
+
+	return json.Marshal(&fullObject)
+}
+
+func CustomMarshalSlice(
+	client graphql.Client,
+	datesss [][][]time.Time,
+	datesssp [][][]*time.Time,
+) (*CustomMarshalSliceResponse, error) {
+	__input := __CustomMarshalSliceInput{
+		Datesss:  datesss,
+		Datesssp: datesssp,
+	}
+	var err error
+
+	var retval CustomMarshalSliceResponse
+	err = client.MakeRequest(
+		nil,
+		"CustomMarshalSlice",
+		`
+query CustomMarshalSlice ($datesss: [[[Date!]!]!]!, $datesssp: [[[Date!]!]!]!) {
+	acceptsListOfListOfListsOfDates(datesss: $datesss)
+	withPointer: acceptsListOfListOfListsOfDates(datesss: $datesssp)
+}
+`,
+		&retval,
+		&__input,
+	)
+	return &retval, err
+}
+
diff --git a/generate/testdata/snapshots/TestGenerate-CustomMarshalSlice.graphql-CustomMarshalSlice.graphql.json b/generate/testdata/snapshots/TestGenerate-CustomMarshalSlice.graphql-CustomMarshalSlice.graphql.json
new file mode 100644
index 0000000..be043c1
--- /dev/null
+++ b/generate/testdata/snapshots/TestGenerate-CustomMarshalSlice.graphql-CustomMarshalSlice.graphql.json
@@ -0,0 +1,9 @@
+{
+  "operations": [
+    {
+      "operationName": "CustomMarshalSlice",
+      "query": "\nquery CustomMarshalSlice ($datesss: [[[Date!]!]!]!, $datesssp: [[[Date!]!]!]!) {\n\tacceptsListOfListOfListsOfDates(datesss: $datesss)\n\twithPointer: acceptsListOfListOfListsOfDates(datesss: $datesssp)\n}\n",
+      "sourceLocation": "testdata/queries/CustomMarshalSlice.graphql"
+    }
+  ]
+}
diff --git a/generate/testdata/snapshots/TestGenerate-InputObject.graphql-InputObject.graphql.go b/generate/testdata/snapshots/TestGenerate-InputObject.graphql-InputObject.graphql.go
index 443070a..b8bc0bf 100644
--- a/generate/testdata/snapshots/TestGenerate-InputObject.graphql-InputObject.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InputObject.graphql-InputObject.graphql.go
@@ -3,6 +3,10 @@ package test
 // Code generated by github.com/Khan/genqlient, DO NOT EDIT.
 
 import (
+	"encoding/json"
+	"fmt"
+	"time"
+
 	"github.com/Khan/genqlient/graphql"
 	"github.com/Khan/genqlient/internal/testutil"
 )
@@ -54,6 +58,32 @@ type UserQueryInput struct {
 	Role       Role             `json:"role"`
 	Names      []string         `json:"names"`
 	HasPokemon testutil.Pokemon `json:"hasPokemon"`
+	Birthdate  time.Time        `json:"-"`
+}
+
+func (v *UserQueryInput) MarshalJSON() ([]byte, error) {
+
+	var fullObject struct {
+		*UserQueryInput
+		Birthdate json.RawMessage `json:"birthdate"`
+		graphql.NoMarshalJSON
+	}
+	fullObject.UserQueryInput = v
+
+	{
+
+		dst := &fullObject.Birthdate
+		src := v.Birthdate
+		var err error
+		*dst, err = testutil.MarshalDate(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"Unable to marshal UserQueryInput.Birthdate: %w", err)
+		}
+	}
+
+	return json.Marshal(&fullObject)
 }
 
 // __InputObjectQueryInput is used internally by genqlient
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go
index b5c531b..351abae 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceListField.graphql-InterfaceListField.graphql.go
@@ -26,6 +26,10 @@ type InterfaceListFieldRootTopic struct {
 
 func (v *InterfaceListFieldRootTopic) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InterfaceListFieldRootTopic
 		Children []json.RawMessage `json:"children"`
@@ -39,15 +43,15 @@ func (v *InterfaceListFieldRootTopic) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Children
-		raw := firstPass.Children
-		*target = make(
+		dst := &v.Children
+		src := firstPass.Children
+		*dst = make(
 			[]InterfaceListFieldRootTopicChildrenContent,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalInterfaceListFieldRootTopicChildrenContent(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal InterfaceListFieldRootTopic.Children: %w", err)
@@ -123,15 +127,15 @@ func (v *InterfaceListFieldRootTopicChildrenTopic) GetId() testutil.ID { return
 // GetName is a part of, and documented with, the interface InterfaceListFieldRootTopicChildrenContent.
 func (v *InterfaceListFieldRootTopicChildrenTopic) GetName() string { return v.Name }
 
-func __unmarshalInterfaceListFieldRootTopicChildrenContent(v *InterfaceListFieldRootTopicChildrenContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInterfaceListFieldRootTopicChildrenContent(b []byte, v *InterfaceListFieldRootTopicChildrenContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -139,13 +143,13 @@ func __unmarshalInterfaceListFieldRootTopicChildrenContent(v *InterfaceListField
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InterfaceListFieldRootTopicChildrenArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InterfaceListFieldRootTopicChildrenVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(InterfaceListFieldRootTopicChildrenTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -181,6 +185,10 @@ type InterfaceListFieldWithPointerTopic struct {
 
 func (v *InterfaceListFieldWithPointerTopic) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InterfaceListFieldWithPointerTopic
 		Children []json.RawMessage `json:"children"`
@@ -194,15 +202,15 @@ func (v *InterfaceListFieldWithPointerTopic) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Children
-		raw := firstPass.Children
-		*target = make(
+		dst := &v.Children
+		src := firstPass.Children
+		*dst = make(
 			[]InterfaceListFieldWithPointerTopicChildrenContent,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalInterfaceListFieldWithPointerTopicChildrenContent(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal InterfaceListFieldWithPointerTopic.Children: %w", err)
@@ -278,15 +286,15 @@ func (v *InterfaceListFieldWithPointerTopicChildrenTopic) GetId() testutil.ID {
 // GetName is a part of, and documented with, the interface InterfaceListFieldWithPointerTopicChildrenContent.
 func (v *InterfaceListFieldWithPointerTopicChildrenTopic) GetName() string { return v.Name }
 
-func __unmarshalInterfaceListFieldWithPointerTopicChildrenContent(v *InterfaceListFieldWithPointerTopicChildrenContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInterfaceListFieldWithPointerTopicChildrenContent(b []byte, v *InterfaceListFieldWithPointerTopicChildrenContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -294,13 +302,13 @@ func __unmarshalInterfaceListFieldWithPointerTopicChildrenContent(v *InterfaceLi
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InterfaceListFieldWithPointerTopicChildrenArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InterfaceListFieldWithPointerTopicChildrenVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(InterfaceListFieldWithPointerTopicChildrenTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go
index 4c195f6..9cc43b1 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceListOfListsOfListsField.graphql-InterfaceListOfListsOfListsField.graphql.go
@@ -86,15 +86,15 @@ func (v *InterfaceListOfListOfListsFieldListOfListsOfListsOfContentTopic) GetNam
 	return v.Name
 }
 
-func __unmarshalInterfaceListOfListOfListsFieldListOfListsOfListsOfContent(v *InterfaceListOfListOfListsFieldListOfListsOfListsOfContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInterfaceListOfListOfListsFieldListOfListsOfListsOfContent(b []byte, v *InterfaceListOfListOfListsFieldListOfListsOfListsOfContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -102,13 +102,13 @@ func __unmarshalInterfaceListOfListOfListsFieldListOfListsOfListsOfContent(v *In
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InterfaceListOfListOfListsFieldListOfListsOfListsOfContentArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InterfaceListOfListOfListsFieldListOfListsOfListsOfContentVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(InterfaceListOfListOfListsFieldListOfListsOfListsOfContentTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -150,6 +150,10 @@ type InterfaceListOfListOfListsFieldResponse struct {
 
 func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InterfaceListOfListOfListsFieldResponse
 		ListOfListsOfListsOfContent [][][]json.RawMessage `json:"listOfListsOfListsOfContent"`
@@ -164,25 +168,25 @@ func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error
 	}
 
 	{
-		target := &v.ListOfListsOfListsOfContent
-		raw := firstPass.ListOfListsOfListsOfContent
-		*target = make(
+		dst := &v.ListOfListsOfListsOfContent
+		src := firstPass.ListOfListsOfListsOfContent
+		*dst = make(
 			[][][]InterfaceListOfListOfListsFieldListOfListsOfListsOfContent,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
-			*target = make(
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
+			*dst = make(
 				[][]InterfaceListOfListOfListsFieldListOfListsOfListsOfContent,
-				len(raw))
-			for i, raw := range raw {
-				target := &(*target)[i]
-				*target = make(
+				len(src))
+			for i, src := range src {
+				dst := &(*dst)[i]
+				*dst = make(
 					[]InterfaceListOfListOfListsFieldListOfListsOfListsOfContent,
-					len(raw))
-				for i, raw := range raw {
-					target := &(*target)[i]
+					len(src))
+				for i, src := range src {
+					dst := &(*dst)[i]
 					err = __unmarshalInterfaceListOfListOfListsFieldListOfListsOfListsOfContent(
-						target, raw)
+						src, dst)
 					if err != nil {
 						return fmt.Errorf(
 							"Unable to unmarshal InterfaceListOfListOfListsFieldResponse.ListOfListsOfListsOfContent: %w", err)
@@ -193,26 +197,26 @@ func (v *InterfaceListOfListOfListsFieldResponse) UnmarshalJSON(b []byte) error
 	}
 
 	{
-		target := &v.WithPointer
-		raw := firstPass.WithPointer
-		*target = make(
+		dst := &v.WithPointer
+		src := firstPass.WithPointer
+		*dst = make(
 			[][][]*InterfaceListOfListOfListsFieldWithPointerContent,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
-			*target = make(
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
+			*dst = make(
 				[][]*InterfaceListOfListOfListsFieldWithPointerContent,
-				len(raw))
-			for i, raw := range raw {
-				target := &(*target)[i]
-				*target = make(
+				len(src))
+			for i, src := range src {
+				dst := &(*dst)[i]
+				*dst = make(
 					[]*InterfaceListOfListOfListsFieldWithPointerContent,
-					len(raw))
-				for i, raw := range raw {
-					target := &(*target)[i]
-					*target = new(InterfaceListOfListOfListsFieldWithPointerContent)
+					len(src))
+				for i, src := range src {
+					dst := &(*dst)[i]
+					*dst = new(InterfaceListOfListOfListsFieldWithPointerContent)
 					err = __unmarshalInterfaceListOfListOfListsFieldWithPointerContent(
-						*target, raw)
+						src, *dst)
 					if err != nil {
 						return fmt.Errorf(
 							"Unable to unmarshal InterfaceListOfListOfListsFieldResponse.WithPointer: %w", err)
@@ -290,15 +294,15 @@ func (v *InterfaceListOfListOfListsFieldWithPointerTopic) GetId() *testutil.ID {
 // GetName is a part of, and documented with, the interface InterfaceListOfListOfListsFieldWithPointerContent.
 func (v *InterfaceListOfListOfListsFieldWithPointerTopic) GetName() *string { return v.Name }
 
-func __unmarshalInterfaceListOfListOfListsFieldWithPointerContent(v *InterfaceListOfListOfListsFieldWithPointerContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInterfaceListOfListOfListsFieldWithPointerContent(b []byte, v *InterfaceListOfListOfListsFieldWithPointerContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -306,13 +310,13 @@ func __unmarshalInterfaceListOfListOfListsFieldWithPointerContent(v *InterfaceLi
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InterfaceListOfListOfListsFieldWithPointerArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InterfaceListOfListOfListsFieldWithPointerVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(InterfaceListOfListOfListsFieldWithPointerTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go
index a2c9a0f..a493d8c 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceNesting.graphql-InterfaceNesting.graphql.go
@@ -24,6 +24,10 @@ type InterfaceNestingRootTopic struct {
 
 func (v *InterfaceNestingRootTopic) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InterfaceNestingRootTopic
 		Children []json.RawMessage `json:"children"`
@@ -37,15 +41,15 @@ func (v *InterfaceNestingRootTopic) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.Children
-		raw := firstPass.Children
-		*target = make(
+		dst := &v.Children
+		src := firstPass.Children
+		*dst = make(
 			[]InterfaceNestingRootTopicChildrenContent,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalInterfaceNestingRootTopicChildrenContent(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal InterfaceNestingRootTopic.Children: %w", err)
@@ -127,15 +131,15 @@ func (v *InterfaceNestingRootTopicChildrenTopic) GetParent() InterfaceNestingRoo
 	return v.Parent
 }
 
-func __unmarshalInterfaceNestingRootTopicChildrenContent(v *InterfaceNestingRootTopicChildrenContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInterfaceNestingRootTopicChildrenContent(b []byte, v *InterfaceNestingRootTopicChildrenContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -143,13 +147,13 @@ func __unmarshalInterfaceNestingRootTopicChildrenContent(v *InterfaceNestingRoot
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InterfaceNestingRootTopicChildrenArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InterfaceNestingRootTopicChildrenVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(InterfaceNestingRootTopicChildrenTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -168,6 +172,10 @@ type InterfaceNestingRootTopicChildrenContentParentTopic struct {
 
 func (v *InterfaceNestingRootTopicChildrenContentParentTopic) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InterfaceNestingRootTopicChildrenContentParentTopic
 		Children []json.RawMessage `json:"children"`
@@ -181,15 +189,15 @@ func (v *InterfaceNestingRootTopicChildrenContentParentTopic) UnmarshalJSON(b []
 	}
 
 	{
-		target := &v.Children
-		raw := firstPass.Children
-		*target = make(
+		dst := &v.Children
+		src := firstPass.Children
+		*dst = make(
 			[]InterfaceNestingRootTopicChildrenContentParentTopicChildrenContent,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalInterfaceNestingRootTopicChildrenContentParentTopicChildrenContent(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal InterfaceNestingRootTopicChildrenContentParentTopic.Children: %w", err)
@@ -265,15 +273,15 @@ func (v *InterfaceNestingRootTopicChildrenContentParentTopicChildrenTopic) GetId
 	return v.Id
 }
 
-func __unmarshalInterfaceNestingRootTopicChildrenContentParentTopicChildrenContent(v *InterfaceNestingRootTopicChildrenContentParentTopicChildrenContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInterfaceNestingRootTopicChildrenContentParentTopicChildrenContent(b []byte, v *InterfaceNestingRootTopicChildrenContentParentTopicChildrenContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -281,13 +289,13 @@ func __unmarshalInterfaceNestingRootTopicChildrenContentParentTopicChildrenConte
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InterfaceNestingRootTopicChildrenContentParentTopicChildrenArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InterfaceNestingRootTopicChildrenContentParentTopicChildrenVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(InterfaceNestingRootTopicChildrenContentParentTopicChildrenTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
diff --git a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go
index 3a41856..b50b156 100644
--- a/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-InterfaceNoFragments.graphql-InterfaceNoFragments.graphql.go
@@ -76,15 +76,15 @@ func (v *InterfaceNoFragmentsQueryRandomItemTopic) GetId() testutil.ID { return
 // GetName is a part of, and documented with, the interface InterfaceNoFragmentsQueryRandomItemContent.
 func (v *InterfaceNoFragmentsQueryRandomItemTopic) GetName() string { return v.Name }
 
-func __unmarshalInterfaceNoFragmentsQueryRandomItemContent(v *InterfaceNoFragmentsQueryRandomItemContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInterfaceNoFragmentsQueryRandomItemContent(b []byte, v *InterfaceNoFragmentsQueryRandomItemContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -92,13 +92,13 @@ func __unmarshalInterfaceNoFragmentsQueryRandomItemContent(v *InterfaceNoFragmen
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InterfaceNoFragmentsQueryRandomItemArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InterfaceNoFragmentsQueryRandomItemVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(InterfaceNoFragmentsQueryRandomItemTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -196,15 +196,15 @@ func (v *InterfaceNoFragmentsQueryRandomItemWithTypeNameTopic) GetId() testutil.
 // GetName is a part of, and documented with, the interface InterfaceNoFragmentsQueryRandomItemWithTypeNameContent.
 func (v *InterfaceNoFragmentsQueryRandomItemWithTypeNameTopic) GetName() string { return v.Name }
 
-func __unmarshalInterfaceNoFragmentsQueryRandomItemWithTypeNameContent(v *InterfaceNoFragmentsQueryRandomItemWithTypeNameContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInterfaceNoFragmentsQueryRandomItemWithTypeNameContent(b []byte, v *InterfaceNoFragmentsQueryRandomItemWithTypeNameContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -212,13 +212,13 @@ func __unmarshalInterfaceNoFragmentsQueryRandomItemWithTypeNameContent(v *Interf
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InterfaceNoFragmentsQueryRandomItemWithTypeNameArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InterfaceNoFragmentsQueryRandomItemWithTypeNameVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(InterfaceNoFragmentsQueryRandomItemWithTypeNameTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -254,6 +254,10 @@ type InterfaceNoFragmentsQueryResponse struct {
 
 func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*InterfaceNoFragmentsQueryResponse
 		RandomItem             json.RawMessage `json:"randomItem"`
@@ -269,10 +273,10 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RandomItem
-		raw := firstPass.RandomItem
+		dst := &v.RandomItem
+		src := firstPass.RandomItem
 		err = __unmarshalInterfaceNoFragmentsQueryRandomItemContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal InterfaceNoFragmentsQueryResponse.RandomItem: %w", err)
@@ -280,10 +284,10 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RandomItemWithTypeName
-		raw := firstPass.RandomItemWithTypeName
+		dst := &v.RandomItemWithTypeName
+		src := firstPass.RandomItemWithTypeName
 		err = __unmarshalInterfaceNoFragmentsQueryRandomItemWithTypeNameContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal InterfaceNoFragmentsQueryResponse.RandomItemWithTypeName: %w", err)
@@ -291,11 +295,11 @@ func (v *InterfaceNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.WithPointer
-		raw := firstPass.WithPointer
-		*target = new(InterfaceNoFragmentsQueryWithPointerContent)
+		dst := &v.WithPointer
+		src := firstPass.WithPointer
+		*dst = new(InterfaceNoFragmentsQueryWithPointerContent)
 		err = __unmarshalInterfaceNoFragmentsQueryWithPointerContent(
-			*target, raw)
+			src, *dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal InterfaceNoFragmentsQueryResponse.WithPointer: %w", err)
@@ -377,15 +381,15 @@ func (v *InterfaceNoFragmentsQueryWithPointerTopic) GetId() *testutil.ID { retur
 // GetName is a part of, and documented with, the interface InterfaceNoFragmentsQueryWithPointerContent.
 func (v *InterfaceNoFragmentsQueryWithPointerTopic) GetName() *string { return v.Name }
 
-func __unmarshalInterfaceNoFragmentsQueryWithPointerContent(v *InterfaceNoFragmentsQueryWithPointerContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalInterfaceNoFragmentsQueryWithPointerContent(b []byte, v *InterfaceNoFragmentsQueryWithPointerContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -393,13 +397,13 @@ func __unmarshalInterfaceNoFragmentsQueryWithPointerContent(v *InterfaceNoFragme
 	switch tn.TypeName {
 	case "Article":
 		*v = new(InterfaceNoFragmentsQueryWithPointerArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(InterfaceNoFragmentsQueryWithPointerVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(InterfaceNoFragmentsQueryWithPointerTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
diff --git a/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go b/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go
index 3b47ae3..0120958 100644
--- a/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-Omitempty.graphql-Omitempty.graphql.go
@@ -3,6 +3,8 @@ package test
 // Code generated by github.com/Khan/genqlient, DO NOT EDIT.
 
 import (
+	"encoding/json"
+	"fmt"
 	"time"
 
 	"github.com/Khan/genqlient/graphql"
@@ -70,6 +72,32 @@ type UserQueryInput struct {
 	Role       Role             `json:"role"`
 	Names      []string         `json:"names"`
 	HasPokemon testutil.Pokemon `json:"hasPokemon"`
+	Birthdate  time.Time        `json:"-"`
+}
+
+func (v *UserQueryInput) MarshalJSON() ([]byte, error) {
+
+	var fullObject struct {
+		*UserQueryInput
+		Birthdate json.RawMessage `json:"birthdate"`
+		graphql.NoMarshalJSON
+	}
+	fullObject.UserQueryInput = v
+
+	{
+
+		dst := &fullObject.Birthdate
+		src := v.Birthdate
+		var err error
+		*dst, err = testutil.MarshalDate(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"Unable to marshal UserQueryInput.Birthdate: %w", err)
+		}
+	}
+
+	return json.Marshal(&fullObject)
 }
 
 // __OmitEmptyQueryInput is used internally by genqlient
diff --git a/generate/testdata/snapshots/TestGenerate-Pointers.graphql-Pointers.graphql.go b/generate/testdata/snapshots/TestGenerate-Pointers.graphql-Pointers.graphql.go
index 96a2a2d..6b6a6f9 100644
--- a/generate/testdata/snapshots/TestGenerate-Pointers.graphql-Pointers.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-Pointers.graphql-Pointers.graphql.go
@@ -3,6 +3,8 @@ package test
 // Code generated by github.com/Khan/genqlient, DO NOT EDIT.
 
 import (
+	"encoding/json"
+	"fmt"
 	"time"
 
 	"github.com/Khan/genqlient/graphql"
@@ -77,6 +79,32 @@ type UserQueryInput struct {
 	Role       *Role             `json:"role"`
 	Names      []*string         `json:"names"`
 	HasPokemon *testutil.Pokemon `json:"hasPokemon"`
+	Birthdate  *time.Time        `json:"-"`
+}
+
+func (v *UserQueryInput) MarshalJSON() ([]byte, error) {
+
+	var fullObject struct {
+		*UserQueryInput
+		Birthdate json.RawMessage `json:"birthdate"`
+		graphql.NoMarshalJSON
+	}
+	fullObject.UserQueryInput = v
+
+	{
+
+		dst := &fullObject.Birthdate
+		src := v.Birthdate
+		var err error
+		*dst, err = testutil.MarshalDate(
+			src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"Unable to marshal UserQueryInput.Birthdate: %w", err)
+		}
+	}
+
+	return json.Marshal(&fullObject)
 }
 
 // __PointersQueryInput is used internally by genqlient
diff --git a/generate/testdata/snapshots/TestGenerate-PointersInline.graphql-PointersInline.graphql.go b/generate/testdata/snapshots/TestGenerate-PointersInline.graphql-PointersInline.graphql.go
index 8ba2afa..bca3927 100644
--- a/generate/testdata/snapshots/TestGenerate-PointersInline.graphql-PointersInline.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-PointersInline.graphql-PointersInline.graphql.go
@@ -3,6 +3,8 @@ package test
 // Code generated by github.com/Khan/genqlient, DO NOT EDIT.
 
 import (
+	"encoding/json"
+	"fmt"
 	"time"
 
 	"github.com/Khan/genqlient/graphql"
@@ -77,6 +79,32 @@ type UserQueryInput struct {
 	Role       Role             `json:"role"`
 	Names      []string         `json:"names"`
 	HasPokemon testutil.Pokemon `json:"hasPokemon"`
+	Birthdate  time.Time        `json:"-"`
+}
+
+func (v *UserQueryInput) MarshalJSON() ([]byte, error) {
+
+	var fullObject struct {
+		*UserQueryInput
+		Birthdate json.RawMessage `json:"birthdate"`
+		graphql.NoMarshalJSON
+	}
+	fullObject.UserQueryInput = v
+
+	{
+
+		dst := &fullObject.Birthdate
+		src := v.Birthdate
+		var err error
+		*dst, err = testutil.MarshalDate(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"Unable to marshal UserQueryInput.Birthdate: %w", err)
+		}
+	}
+
+	return json.Marshal(&fullObject)
 }
 
 // __PointersQueryInput is used internally by genqlient
diff --git a/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go
index 08a4358..74f4cfd 100644
--- a/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-SimpleInlineFragment.graphql-SimpleInlineFragment.graphql.go
@@ -77,15 +77,15 @@ func (v *SimpleInlineFragmentRandomItemTopic) GetId() testutil.ID { return v.Id
 // GetName is a part of, and documented with, the interface SimpleInlineFragmentRandomItemContent.
 func (v *SimpleInlineFragmentRandomItemTopic) GetName() string { return v.Name }
 
-func __unmarshalSimpleInlineFragmentRandomItemContent(v *SimpleInlineFragmentRandomItemContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalSimpleInlineFragmentRandomItemContent(b []byte, v *SimpleInlineFragmentRandomItemContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -93,13 +93,13 @@ func __unmarshalSimpleInlineFragmentRandomItemContent(v *SimpleInlineFragmentRan
 	switch tn.TypeName {
 	case "Article":
 		*v = new(SimpleInlineFragmentRandomItemArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(SimpleInlineFragmentRandomItemVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(SimpleInlineFragmentRandomItemTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -133,6 +133,10 @@ type SimpleInlineFragmentResponse struct {
 
 func (v *SimpleInlineFragmentResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*SimpleInlineFragmentResponse
 		RandomItem json.RawMessage `json:"randomItem"`
@@ -146,10 +150,10 @@ func (v *SimpleInlineFragmentResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RandomItem
-		raw := firstPass.RandomItem
+		dst := &v.RandomItem
+		src := firstPass.RandomItem
 		err = __unmarshalSimpleInlineFragmentRandomItemContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal SimpleInlineFragmentResponse.RandomItem: %w", err)
diff --git a/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go
index b23ef6f..f8e1583 100644
--- a/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-SimpleNamedFragment.graphql-SimpleNamedFragment.graphql.go
@@ -76,15 +76,15 @@ func (v *SimpleNamedFragmentRandomItemTopic) GetId() testutil.ID { return v.Id }
 // GetName is a part of, and documented with, the interface SimpleNamedFragmentRandomItemContent.
 func (v *SimpleNamedFragmentRandomItemTopic) GetName() string { return v.Name }
 
-func __unmarshalSimpleNamedFragmentRandomItemContent(v *SimpleNamedFragmentRandomItemContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalSimpleNamedFragmentRandomItemContent(b []byte, v *SimpleNamedFragmentRandomItemContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -92,13 +92,13 @@ func __unmarshalSimpleNamedFragmentRandomItemContent(v *SimpleNamedFragmentRando
 	switch tn.TypeName {
 	case "Article":
 		*v = new(SimpleNamedFragmentRandomItemArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(SimpleNamedFragmentRandomItemVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(SimpleNamedFragmentRandomItemTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -127,6 +127,10 @@ type SimpleNamedFragmentRandomItemVideo struct {
 
 func (v *SimpleNamedFragmentRandomItemVideo) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*SimpleNamedFragmentRandomItemVideo
 		graphql.NoUnmarshalJSON
@@ -177,15 +181,15 @@ func (v *SimpleNamedFragmentRandomLeafVideo) implementsGraphQLInterfaceSimpleNam
 // GetTypename is a part of, and documented with, the interface SimpleNamedFragmentRandomLeafLeafContent.
 func (v *SimpleNamedFragmentRandomLeafVideo) GetTypename() string { return v.Typename }
 
-func __unmarshalSimpleNamedFragmentRandomLeafLeafContent(v *SimpleNamedFragmentRandomLeafLeafContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalSimpleNamedFragmentRandomLeafLeafContent(b []byte, v *SimpleNamedFragmentRandomLeafLeafContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -193,10 +197,10 @@ func __unmarshalSimpleNamedFragmentRandomLeafLeafContent(v *SimpleNamedFragmentR
 	switch tn.TypeName {
 	case "Article":
 		*v = new(SimpleNamedFragmentRandomLeafArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(SimpleNamedFragmentRandomLeafVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing LeafContent.__typename")
@@ -214,6 +218,10 @@ type SimpleNamedFragmentRandomLeafVideo struct {
 
 func (v *SimpleNamedFragmentRandomLeafVideo) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*SimpleNamedFragmentRandomLeafVideo
 		graphql.NoUnmarshalJSON
@@ -241,6 +249,10 @@ type SimpleNamedFragmentResponse struct {
 
 func (v *SimpleNamedFragmentResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*SimpleNamedFragmentResponse
 		RandomItem json.RawMessage `json:"randomItem"`
@@ -255,10 +267,10 @@ func (v *SimpleNamedFragmentResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RandomItem
-		raw := firstPass.RandomItem
+		dst := &v.RandomItem
+		src := firstPass.RandomItem
 		err = __unmarshalSimpleNamedFragmentRandomItemContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal SimpleNamedFragmentResponse.RandomItem: %w", err)
@@ -266,10 +278,10 @@ func (v *SimpleNamedFragmentResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RandomLeaf
-		raw := firstPass.RandomLeaf
+		dst := &v.RandomLeaf
+		src := firstPass.RandomLeaf
 		err = __unmarshalSimpleNamedFragmentRandomLeafLeafContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal SimpleNamedFragmentResponse.RandomLeaf: %w", err)
diff --git a/generate/testdata/snapshots/TestGenerate-StructOption.graphql-StructOption.graphql.go b/generate/testdata/snapshots/TestGenerate-StructOption.graphql-StructOption.graphql.go
index ff55649..7c6b631 100644
--- a/generate/testdata/snapshots/TestGenerate-StructOption.graphql-StructOption.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-StructOption.graphql-StructOption.graphql.go
@@ -62,6 +62,10 @@ type StructOptionRootTopicChildrenContentParentTopic struct {
 
 func (v *StructOptionRootTopicChildrenContentParentTopic) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*StructOptionRootTopicChildrenContentParentTopic
 		InterfaceChildren []json.RawMessage `json:"interfaceChildren"`
@@ -75,15 +79,15 @@ func (v *StructOptionRootTopicChildrenContentParentTopic) UnmarshalJSON(b []byte
 	}
 
 	{
-		target := &v.InterfaceChildren
-		raw := firstPass.InterfaceChildren
-		*target = make(
+		dst := &v.InterfaceChildren
+		src := firstPass.InterfaceChildren
+		*dst = make(
 			[]StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent,
-			len(raw))
-		for i, raw := range raw {
-			target := &(*target)[i]
+			len(src))
+		for i, src := range src {
+			dst := &(*dst)[i]
 			err = __unmarshalStructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent(
-				target, raw)
+				src, dst)
 			if err != nil {
 				return fmt.Errorf(
 					"Unable to unmarshal StructOptionRootTopicChildrenContentParentTopic.InterfaceChildren: %w", err)
@@ -169,15 +173,15 @@ func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenTopic)
 	return v.Id
 }
 
-func __unmarshalStructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent(v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalStructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent(b []byte, v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -185,13 +189,13 @@ func __unmarshalStructOptionRootTopicChildrenContentParentTopicInterfaceChildren
 	switch tn.TypeName {
 	case "Article":
 		*v = new(StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -218,6 +222,10 @@ type StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo struc
 
 func (v *StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*StructOptionRootTopicChildrenContentParentTopicInterfaceChildrenVideo
 		graphql.NoUnmarshalJSON
diff --git a/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go
index 4445b53..fabe85f 100644
--- a/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-TypeNames.graphql-TypeNames.graphql.go
@@ -65,15 +65,15 @@ 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" {
+func __unmarshalItem(b []byte, v *Item) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -81,13 +81,13 @@ func __unmarshalItem(v *Item, m json.RawMessage) error {
 	switch tn.TypeName {
 	case "Article":
 		*v = new(ItemArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(ItemVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Topic":
 		*v = new(ItemTopic)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing Content.__typename")
@@ -134,6 +134,10 @@ type Resp struct {
 
 func (v *Resp) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*Resp
 		RandomItem json.RawMessage `json:"randomItem"`
@@ -147,10 +151,10 @@ func (v *Resp) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RandomItem
-		raw := firstPass.RandomItem
+		dst := &v.RandomItem
+		src := firstPass.RandomItem
 		err = __unmarshalItem(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal Resp.RandomItem: %w", err)
diff --git a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go
index d802409..657fe48 100644
--- a/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-UnionNoFragments.graphql-UnionNoFragments.graphql.go
@@ -40,15 +40,15 @@ func (v *UnionNoFragmentsQueryRandomLeafVideo) implementsGraphQLInterfaceUnionNo
 // GetTypename is a part of, and documented with, the interface UnionNoFragmentsQueryRandomLeafLeafContent.
 func (v *UnionNoFragmentsQueryRandomLeafVideo) GetTypename() string { return v.Typename }
 
-func __unmarshalUnionNoFragmentsQueryRandomLeafLeafContent(v *UnionNoFragmentsQueryRandomLeafLeafContent, m json.RawMessage) error {
-	if string(m) == "null" {
+func __unmarshalUnionNoFragmentsQueryRandomLeafLeafContent(b []byte, v *UnionNoFragmentsQueryRandomLeafLeafContent) error {
+	if string(b) == "null" {
 		return nil
 	}
 
 	var tn struct {
 		TypeName string `json:"__typename"`
 	}
-	err := json.Unmarshal(m, &tn)
+	err := json.Unmarshal(b, &tn)
 	if err != nil {
 		return err
 	}
@@ -56,10 +56,10 @@ func __unmarshalUnionNoFragmentsQueryRandomLeafLeafContent(v *UnionNoFragmentsQu
 	switch tn.TypeName {
 	case "Article":
 		*v = new(UnionNoFragmentsQueryRandomLeafArticle)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "Video":
 		*v = new(UnionNoFragmentsQueryRandomLeafVideo)
-		return json.Unmarshal(m, *v)
+		return json.Unmarshal(b, *v)
 	case "":
 		return fmt.Errorf(
 			"Response was missing LeafContent.__typename")
@@ -81,6 +81,10 @@ type UnionNoFragmentsQueryResponse struct {
 
 func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 
+	if string(b) == "null" {
+		return nil
+	}
+
 	var firstPass struct {
 		*UnionNoFragmentsQueryResponse
 		RandomLeaf json.RawMessage `json:"randomLeaf"`
@@ -94,10 +98,10 @@ func (v *UnionNoFragmentsQueryResponse) UnmarshalJSON(b []byte) error {
 	}
 
 	{
-		target := &v.RandomLeaf
-		raw := firstPass.RandomLeaf
+		dst := &v.RandomLeaf
+		src := firstPass.RandomLeaf
 		err = __unmarshalUnionNoFragmentsQueryRandomLeafLeafContent(
-			target, raw)
+			src, dst)
 		if err != nil {
 			return fmt.Errorf(
 				"Unable to unmarshal UnionNoFragmentsQueryResponse.RandomLeaf: %w", err)
diff --git a/generate/testdata/snapshots/TestGenerate-unexported.graphql-unexported.graphql.go b/generate/testdata/snapshots/TestGenerate-unexported.graphql-unexported.graphql.go
index 4994cbb..e0267f8 100644
--- a/generate/testdata/snapshots/TestGenerate-unexported.graphql-unexported.graphql.go
+++ b/generate/testdata/snapshots/TestGenerate-unexported.graphql-unexported.graphql.go
@@ -3,6 +3,10 @@ package test
 // Code generated by github.com/Khan/genqlient, DO NOT EDIT.
 
 import (
+	"encoding/json"
+	"fmt"
+	"time"
+
 	"github.com/Khan/genqlient/graphql"
 	"github.com/Khan/genqlient/internal/testutil"
 )
@@ -34,6 +38,32 @@ type UserQueryInput struct {
 	Role       Role             `json:"role"`
 	Names      []string         `json:"names"`
 	HasPokemon testutil.Pokemon `json:"hasPokemon"`
+	Birthdate  time.Time        `json:"-"`
+}
+
+func (v *UserQueryInput) MarshalJSON() ([]byte, error) {
+
+	var fullObject struct {
+		*UserQueryInput
+		Birthdate json.RawMessage `json:"birthdate"`
+		graphql.NoMarshalJSON
+	}
+	fullObject.UserQueryInput = v
+
+	{
+
+		dst := &fullObject.Birthdate
+		src := v.Birthdate
+		var err error
+		*dst, err = testutil.MarshalDate(
+			&src)
+		if err != nil {
+			return nil, fmt.Errorf(
+				"Unable to marshal UserQueryInput.Birthdate: %w", err)
+		}
+	}
+
+	return json.Marshal(&fullObject)
 }
 
 // __unexportedInput is used internally by genqlient
diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go
index 34ff6b1..41b1dde 100644
--- a/internal/integration/integration_test.go
+++ b/internal/integration/integration_test.go
@@ -9,6 +9,7 @@ import (
 	"context"
 	"net/http"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -116,6 +117,64 @@ func TestOmitempty(t *testing.T) {
 	assert.Equal(t, 17, resp.User.LuckyNumber)
 }
 
+func TestCustomMarshal(t *testing.T) {
+	_ = `# @genqlient
+	query queryWithCustomMarshal($date: Date!) {
+		usersBornOn(date: $date) { id name birthdate }
+	}`
+
+	ctx := context.Background()
+	server := server.RunServer()
+	defer server.Close()
+	client := graphql.NewClient(server.URL, http.DefaultClient)
+
+	resp, err := queryWithCustomMarshal(ctx, client,
+		time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC))
+	require.NoError(t, err)
+
+	assert.Len(t, resp.UsersBornOn, 1)
+	user := resp.UsersBornOn[0]
+	assert.Equal(t, "1", user.Id)
+	assert.Equal(t, "Yours Truly", user.Name)
+	assert.Equal(t,
+		time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC),
+		user.Birthdate)
+
+	resp, err = queryWithCustomMarshal(ctx, client,
+		time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC))
+	require.NoError(t, err)
+	assert.Len(t, resp.UsersBornOn, 0)
+}
+
+func TestCustomMarshalSlice(t *testing.T) {
+	_ = `# @genqlient
+	query queryWithCustomMarshalSlice($dates: [Date!]!) {
+		usersBornOnDates(dates: $dates) { id name birthdate }
+	}`
+
+	ctx := context.Background()
+	server := server.RunServer()
+	defer server.Close()
+	client := graphql.NewClient(server.URL, http.DefaultClient)
+
+	resp, err := queryWithCustomMarshalSlice(ctx, client,
+		[]time.Time{time.Date(2025, time.January, 1, 12, 34, 56, 789, time.UTC)})
+	require.NoError(t, err)
+
+	assert.Len(t, resp.UsersBornOnDates, 1)
+	user := resp.UsersBornOnDates[0]
+	assert.Equal(t, "1", user.Id)
+	assert.Equal(t, "Yours Truly", user.Name)
+	assert.Equal(t,
+		time.Date(2025, time.January, 1, 0, 0, 0, 0, time.UTC),
+		user.Birthdate)
+
+	resp, err = queryWithCustomMarshalSlice(ctx, client,
+		[]time.Time{time.Date(2021, time.January, 1, 12, 34, 56, 789, time.UTC)})
+	require.NoError(t, err)
+	assert.Len(t, resp.UsersBornOnDates, 0)
+}
+
 func TestInterfaceNoFragments(t *testing.T) {
 	_ = `# @genqlient
 	query queryWithInterfaceNoFragments($id: ID!) {
diff --git a/internal/testutil/types.go b/internal/testutil/types.go
index a4b44d3..9d5243c 100644
--- a/internal/testutil/types.go
+++ b/internal/testutil/types.go
@@ -2,6 +2,7 @@ package testutil
 
 import (
 	"context"
+	"time"
 
 	"github.com/Khan/genqlient/graphql"
 )
@@ -26,3 +27,20 @@ type MyContext interface {
 func GetClientFromNowhere() (graphql.Client, error)                    { return nil, nil }
 func GetClientFromContext(ctx context.Context) (graphql.Client, error) { return nil, nil }
 func GetClientFromMyContext(ctx MyContext) (graphql.Client, error)     { return nil, nil }
+
+const dateFormat = "2006-01-02"
+
+func MarshalDate(t *time.Time) ([]byte, error) {
+	return []byte(`"` + t.Format(dateFormat) + `"`), nil
+}
+
+func UnmarshalDate(b []byte, t *time.Time) error {
+	// (modified from time.Time.UnmarshalJSON)
+	if string(b) == "null" {
+		return nil
+	}
+
+	var err error
+	*t, err = time.Parse(`"`+dateFormat+`"`, string(b))
+	return err
+}