In a GraphQL multipart request, what do the operations and map fields do?
answer
- Three roles in one request body
- The JSON operation still has holes
- Something has to say where bytes land
- Keys name parts, values name paths
- Order is normative, for streaming
basics
~20 sThe operations field holds the JSON operation with null wherever a file belongs. The map field binds each numbered file part to the variable path that null sits at. The server substitutes the bytes into the variables before executing.
solid answer
~50 sThe GraphQL multipart request specification, a community document, splits the request into three roles. `operations` is the JSON-encoded operation — the same object a JSON request would carry — with `null` at every variable position a file will occupy. `map` is a JSON-encoded object whose keys are the names of the file fields that follow and whose values are **arrays** of dot-separated paths into that operation, such as `variables.photos.0`; the array allows one uploaded file to be substituted at several positions and travel the wire once. Then come the file fields themselves. The server decodes the two JSON fields, replaces each `null` with the corresponding file, and only then coerces variables and executes — so a variable declared `[Upload!]!` never sees the placeholder. The ordering is normative, not cosmetic: `operations`, then `map`, then files, so the server holds the complete routing table before any file byte arrives and can place it without buffering the whole body.
code
graphql · 6 linesmutation AttachPhotos($accessionId: ID!, $photos: [Upload!]!) {
attachConditionPhotos(accessionId: $accessionId, photos: $photos) {
id
photoCount
}
}go deeper
Recall the three roles: a JSON operation with holes in it, a map saying which file fills which hole, and the files themselves. You are not expected to write the body by hand, only to read one and explain what each part is for.
Explain the path syntax, why a map value is an array of paths, and why the null placeholder never reaches variable coercion. Knowing that operations and map must precede the file fields, and why, is the detail that separates use from understanding.
Show that you can debug this: a mapping bug surfaces as a request error with no path, a reordered body fails only against a streaming server, and client-declared filenames and content types are untrusted input. Expect to be asked what breaks at hops that assume a JSON body.
Own the consequence that this path is a special case everywhere it crosses — intermediaries, document-hash handshakes, JSON-only composition layers. Decide deliberately whether that special case earns its keep or whether uploads belong outside the graph.
## The problem the three fields solve An operation is structured text and a file is bytes, and one request has to carry both. `multipart/form-data` lets a single body hold several named fields, some textual and some binary; the GraphQL-specific part of the convention is not that encoding but the **three roles** it assigns to fields, and the rule that binds them together. This is a community specification. It is not in the GraphQL specification and not in the GraphQL over HTTP working draft, so "where is this defined" is a fair interview question and "the spec" is the wrong answer. ## `operations` A JSON-encoded operations object: the same `query`, `variables` and `operationName` an ordinary JSON request would carry. The one difference is that every variable position a file will occupy holds `null`. That `null` is a **placeholder, not a value**. Substitution happens before the server coerces variables against their declared types, so a variable declared `[Upload!]!` never sees it. Candidates often expect a nullability error here and are surprised there is none. A JSON array in this field means several operations in one request; the paths in `map` then begin with the operation's index. That batching shape is a subject of its own — what matters here is only that the path syntax has to accommodate it. ## `map` A JSON-encoded object whose **keys** are the names of the file fields that follow — `"0"`, `"1"`, `"2"` by convention — and whose **values** are arrays of dot-separated paths into the operations object: `"variables.photo"`, `"variables.photos.0"`, and for a batched request `"1.variables.photo"`. Numeric segments index arrays. Two details are worth knowing precisely. First, the value is an **array of paths**, not one path, so a single uploaded file can be substituted at several positions — one colour-reference target used by two variables, or the same file needed by two batched operations — while crossing the wire once. Second, the map is the **only** binding between a file field and the operation. The field names carry no meaning of their own; the server does not infer anything from a filename or a field name, and a path in the map that does not exist in the operations object is a request error, rejected before execution, so the resulting `errors` entry has no path into `data` and nothing points at the mutation. ## The file fields One per file, named by the map keys, each carrying the bytes plus a client-declared filename and content type. Both of those are **claims made by the caller**. A resolver that trusts the declared content type is trusting the uploader; the check has to be against the bytes. ## The ordering is normative The convention requires `operations` first, `map` second, and the file fields after. This is not tidiness. Read in that order, the server holds the complete routing table before the first byte of the first file arrives, so it can pipe each file straight to where it belongs — a temporary file, an object store, a scanner — without buffering the entire request in memory. Field order is what makes the request **processable as a stream**. **A failure worth carrying with you.** In a museum collection graph, a digitisation bench uploaded condition photos through a hand-built request that appended the two JSON fields *after* the file fields. It passed every local test, because the development server buffered the whole body and then read the fields as a dictionary, where order is invisible. It failed in the shared environment against a server that read the body as a stream: the first file part arrived with no map, and the request was rejected outright, before any resolver ran, with an error that pointed at nothing. The assumption that broke was that form fields are a set. On this convention they are a sequence — and a server that tolerates a different order is being lenient, not compliant. ## The `Upload` scalar The convention describes the request, not the schema. The `Upload` scalar that servers pair with it is a separate convention: input-only, because there is no sensible way to serialize a file into a JSON response, and with no literal form, because a file cannot be written into a document. Whatever the resolver receives — a stream, a temporary file, a buffer — is defined by the server, not by any specification, so the SDL ports between servers and the resolver body does not. ## What stops working around it The body is no longer JSON, and that is the operational cost. Anything that assumed JSON needs explicit support for this one path: an intermediary that inspects or rewrites bodies, a document-hash handshake, a JSON-only composition layer in front of subgraphs. The upload becomes a special case at every hop it crosses, which is a large part of why teams eventually move large files out of the graph entirely.
- The operations field has null where a file goes — why does a non-null variable not fail validation?Because the null is never coerced. The server decodes `operations` and `map`, substitutes each uploaded file into the variable values at the mapped paths, and only then parses, validates and executes the operation with the completed variables. A variable declared `[Upload!]!` sees files, not placeholders. The nulls are a transport artefact of encoding the operation as JSON, not values the execution algorithm ever observes.
- What happens if a map path points at a position the operation does not contain?It is a request error. The server cannot build the variable values, so nothing is parsed, validated or executed: the response carries an `errors` entry with no path into `data`, and `data` is absent rather than partially filled. The practical consequence is that a mapping bug looks nothing like a resolver failure — there is no field to attribute it to, and no partial result to inspect.
- Should a resolver trust the filename and content type carried on a file field?No. Both are supplied by the caller and neither is verified by the convention or by the transport. Treat them as hints for display at most: sniff the actual bytes to determine the real type, generate your own storage key rather than reusing the supplied name, and never let a client-supplied name reach a filesystem path or a URL unchanged. The convention describes routing, not validation.
The operations field is a form with blanks, the map is the instruction sheet saying which photograph goes in which blank, and the file fields are the photographs.
saying these in an interview costs you the question
- Thinks the file field names bind files to variables
- Expects a null placeholder to fail non-null validation
- Says field order in the request is cosmetic
- Believes the map value is a single path string
- Trusts the client-declared filename or content type
- Attributes the multipart convention to the GraphQL specification