Summary

  • The JMAP Enhanced Result References draft allows values selected from an earlier method response to populate later object properties, patch values and query filters, using JSON Pointer or optional JSON Path.
  • Resolution defines cardinality and type behavior—including zero matches becoming null, [] or {}—but it does not establish provenance, freshness, destination authority or the meaning of an empty value.

An automation requested a list, selected one attribute from the response, and fed it into the next call. The selector was designed to find a single matching object. On Tuesday it found none. For a destination expecting one primitive value, the draft says that a zero-node JSON Path result becomes null. The receiving property permitted null, and the update completed.

The incident review began with the wrong question: why had JSON Path failed? It had not. The selector produced the cardinality it observed, the resolver mapped that cardinality according to the destination type, and the method validated the resulting JSON value. Every technical layer kept its contract. The application had quietly supplied the missing rule: “no match means clear the current value.”

That rule did not come from the evidence.

This is the important boundary in JMAP Enhanced Result References. Revision 02 is dated 21 June 2026 and expires on 23 December 2026. At the research cut-off it was an active JMAP Working Group Internet-Draft on the Standards Track path. It extends the result-reference machinery in RFC 8620; it is not a final RFC, an implementation certificate or a measurement of deployment.

The draft solves a real coordination problem. JMAP requests can contain several method calls, executed in sequence. A later call may need a value returned by an earlier one: an identifier, a property, an array of objects or a value used to narrow a query. A result reference identifies the earlier call through resultOf and name, then selects data from that response with path.

The enhancement lets that reference appear inside properties supplied to /set, in PatchObject values and inside FilterCondition objects used by /query. It also adds optional JSON Path support alongside JSON Pointer. The capability gives a client more expressive dataflow without requiring it to stop, parse one response and construct another request.

Convenience, however, is not provenance.

Three different meanings of empty

JSON Path can return a nodelist with zero, one or many nodes. The resolver must turn that list into the type expected by the destination. For a primitive or single-object property, exactly one node becomes the value, zero nodes become null, and more than one is an invalidResultReference. For an array property, zero nodes become []; one or more become an array in JSON Path order. For a map, zero becomes {}, while exactly one conforming object is accepted.

These are precise shape rules. They prevent each implementation from improvising how a nodelist enters a typed JMAP method. They do not make the three empty values interchangeable.

null can mean absent, clear this property, unknown or invalid, depending on the destination contract. An empty array can mean there were no members, replace the membership set with none, or match no objects. An empty map can mean no overrides, erase all named entries, or simply no keys were selected. The resolver knows the expected JSON shape. It does not know which consequence the business process assigns to emptiness.

JSON Pointer makes the distinction sharper. A pointer without a wildcard that names no value is an error. Under the draft's extended wildcard behavior, zero matches may become the same typed empty values used for a nodelist. Two expressions can therefore look like alternate ways to reach “the same place” while giving absence different control-flow consequences.

A safe application cannot treat empty resolution as a generic success. It has to decide, property by property and filter by filter, whether zero is an admissible observation, a no-op, an explicit clearing instruction or a reason to stop. That decision belongs close to the destination semantics, not inside a method-agnostic selector.

Correct type is the beginning of validation

After resolution, the server validates the value against the destination type. A mismatched property in /set is rejected as invalidProperties; a mismatched FilterCondition in /query is rejected as invalidArguments. The draft is explicit that implementations must not turn strings into numbers or booleans, or serialize complex objects into strings, merely to obtain a value the next method will accept.

That prohibition is more than parser hygiene. Coercion can manufacture an evidentiary claim. If a prior response contains the string "01" and a later filter expects a number, silently converting it to 1 discards formatting, domain and identifier semantics. A rejection says the evidence did not satisfy the declared contract. A coercion pretends it did.

Even strict type checking reaches only so far. A mailbox identifier may be a perfectly valid string and still belong to the wrong account. A blob identifier may have the correct syntax and still point to content that the later audience is not allowed to receive. A Boolean can be structurally valid while representing a decision made under a superseded policy.

The evidence chain therefore needs at least four questions. What response produced the value? What selector found it, and with what cardinality? What destination type accepted it? What authority allowed that value to govern this later operation? The draft answers the middle two particularly well. Systems must not use that precision to imply answers to the other two.

A thin resolver should remain thin

One of the draft's strongest architectural choices is easy to miss. It permits a syntactic intermediate layer to resolve references over opaque JSON without knowing JMAP method types, property definitions or business semantics. That layer can locate the earlier response and apply JSON Pointer or JSON Path. A later method-execution layer can then interpret the value against its expected type and rules.

This separation lets proxies, gateways and common server components implement dataflow once. It also creates a clean evidentiary statement. The intermediate layer can honestly say: this expression, applied to this earlier response, produced this nodelist. It cannot honestly say: this was the right value for the next action.

Heng Lu's thin-layer discipline is useful here. A coordination component becomes dangerous when technical participation is promoted into mandate. The resolver participates in moving JSON. The method participates in accepting a typed argument. Neither participation identifies the human or institution entitled to bind the destination, widen an audience or erase an existing value.

Keeping the layer thin does not mean keeping the audit thin. An inspectable receipt should preserve resultOf, method name, source account and security context, selector syntax, selected-node count, resolved JSON type, destination property or filter, validation result and authorization outcome. If the system stores only the final object, the most consequential part of the decision disappears: why that value arrived there.

Cross-context copying is the real security test

Consider a prior Email/get response that includes a private attachment's blob identifier. A later /set call inserts the selected object or identifier into a record shared with a wider group. The result reference resolves. The destination type validates. The later write may even be authorized in isolation.

The unresolved question is whether the principal was entitled to move the source data into that destination context.

Read permission and write permission are not automatically a transfer mandate. A user may be able to see a document and edit a public record without being allowed to publish the document through that record. A service account may read across tenants for support work and write within one tenant, while being forbidden to connect the two. A selector can carry the data across the boundary without ever representing the boundary.

This is why the draft's security discussion warns about cross-context leakage and asks audit systems to record which data derived from references. The most useful control is not a broad “result references enabled” switch. It is destination-aware authorization that sees the source context and the intended sink together.

A capability advertisement cannot supply that judgment. urn:ietf:params:jmap:refplus says a server understands the extension. An account capability can say whether JSON Path is supported. Those facts establish technical availability. They do not authorize every selector or every movement of selected content.

Expressiveness spends resources

JSON Pointer is deliberately simple: it follows an exact structural route. JSON Path can filter, select multiple nodes and use recursive descent. That power makes it suitable for extracting values from variable result structures. It also turns an expression into a unit of computational work.

A recursive selector over a large response can produce a huge nodelist. Nested result references can copy complex values repeatedly. FilterCondition references can multiply evaluation inside a query. Sequential method execution prevents the simplest direct circular reference, but it does not prevent an attacker or a careless client from constructing expensive chains.

Servers therefore need limits on expression complexity, evaluation time, nodelist size, total references, nesting depth and cumulative request cost. A limit exceeded should remain a visible invalidResultReference, not be “recovered” by truncating the selected set. Truncation would change the evidence while preserving the appearance of success.

Parser choice also belongs in the risk model. JSON Path is a language, not only a string format. Maintained libraries, security updates, isolation and predictable limit enforcement are part of the control surface. A gateway that treats expressions as harmless metadata can become the most expensive component in the request path.

A cache hit must carry its security epoch

Resolution looks cacheable. The source response, expression and result may be identical across repeated calls. But a cache key based only on bytes and path is incomplete when the value's permissibility depends on identity, account, access-control state or destination.

Suppose a user selects an identifier while they are a member of a private workspace. An administrator removes that membership. A later request reuses the cached selection after the permission change. The JSON remains correct, and the path still points to the same node. The authorization evidence is stale.

The draft requires caches not to be shared across users or security contexts and calls for invalidation when access control changes. In practice, the receipt should bind the cache entry to the principal, account, relevant permissions and policy epoch. Operators should be able to distinguish a fresh evaluation from a cached one and see why reuse was allowed.

Timing matters too. Cache behavior can reveal whether a value or path existed in another context. Isolation and timing defenses are necessary even when the cached payload itself is never returned directly.

Preserve the negative claim

The strongest system is not the one that writes the largest success statement. It is the one that preserves what each layer did not prove.

A successful reference says the server located the named prior result and evaluated the selector. Cardinality rules say how the match set became a destination-shaped value. Type validation says the value conformed to the declared JSON contract. Destination authorization says the later method was permitted. None of those alone proves the value was fresh, that the source was authoritative for the decision, that an empty result carried the intended meaning, or that an external audience experienced the desired outcome.

Running code should make these boundaries visible. Exercise zero, one and multiple matches against scalar, array and map destinations. Compare missing exact pointers with empty wildcard results. Inject correctly typed identifiers from the wrong account. Change access rights between resolution and cache reuse. Send expensive recursive expressions and confirm that limits reject rather than truncate. Preserve the source lineage, then prove an auditor can reconstruct the final write from it.

The selector that found nothing did not lie. The resolver that produced null did not improvise. The error began when the application converted a precise absence into an unreviewed instruction. Enhanced result references can make JMAP dataflow more expressive. Their legitimacy depends on keeping selection, meaning and authority separate.

Sources