02 - Shapes
A shape says which fields of a result the caller wants. Shapes are what make Rayfold client-shaped without giving up HTTP caching: a shape is canonicalised, hashed and can be persisted, so a read becomes a GET/QUERY on a stable key.
1. Grammar
shape := "{" item* "}"
item := field | spread | defer
field := (alias ":")? name args? shape? modifier*
args := "(" (name ":" value)* ")"
value := literal | "$" name // $name resolves from the op's `vars`
spread := "..." Type "." viewName // named view of that type
| "..." "on" Type shape // type condition (unions, interfaces)
defer := "@defer" ("(" "label" ":" string ")")? shape
modifier := "@eager" | "@partial"Example:
{
id
title
author { id name }
reviews(first: $n) { items { id rating } cursor hasMore }
...Book.card
@defer(label: "stats") { salesRank }
...on Author { bio }
}2. Semantics
- A field that is not selected is not resolved. A field with no sub-shape on an entity or object type receives that type's
defaultview (01 §2.7). So{ id author }is legal and returns the author's default view. - No shape at all means the operation result's
defaultview. alias: namerenames the field in the output. Two selections of the same field with different args MUST use aliases.- Spreads are flattened. A named-view spread copies the view's items. A type-condition spread contributes only when the concrete
$typematches. @deferblocks are resolved after the enclosing frame is sent and delivered aspatch-freedataframes addressed byat(04 §3). Fields annotated@lazyin the schema behave as if wrapped in@deferunless the shape marks them@eager.@partialon a field lets that field fail independently (05 §4).- Every object in a result carries
"$type"when the static type is an entity, union or interface; implementations MAY omit it for plain objects.
3. Canonical form and hashing
The canonical text of a shape is produced by:
- Expanding named-view spreads (type-condition spreads stay).
- Sorting items at each level by output name (alias or field name), then by canonical args.
- Sorting args by name; encoding literal values as canonical JSON; keeping
$namereferences verbatim. - Emitting with single spaces, no newlines, no commas:
{ author { id name } id title }.
The shape id is sha256: + lowercase hex SHA-256 of the canonical UTF-8 text.
An operation refers to its shape either inline ("shape": "{ id title }") or by id ("shape": "sha256:..."). Servers MUST accept inline shapes in development mode and MAY refuse them in production (trusted shapes): the allowlist is the set of shape ids registered by rayfold shapes at client build time and shipped alongside the schema.
4. Variables
$name inside shape args resolves from the operation's vars object. Persisted shapes keep their id regardless of variable values; the cache key (07) includes args and vars.
5. Limits
Servers MUST enforce a maximum nesting depth (default 8), a maximum number of selected fields (default 500) and the cost budget (06 §5). Exceeding a limit is resource_exhausted before execution.