jmap-types
Shared JMAP wire types for the jmap-* crate family.
Implements the core data structures defined in RFC 8620 (JMAP Core): identifiers, timestamps, state tokens, method-level errors, request/response envelopes, and result references. All types serialize to and from the exact JSON wire format the spec requires.
What
| Type | RFC | Description |
|---|---|---|
Id |
§1.2 | Opaque server-assigned record identifier |
UTCDate |
§1.4 | RFC 3339 UTC timestamp string |
State |
§1.2 | Opaque server state token |
JmapError |
§3.6.2 | Method-level error with typed constructors |
Invocation |
§3.2 | (method_name, arguments, call_id) 3-tuple |
JmapRequest |
§3.3 | HTTP request envelope |
JmapResponse |
§3.4 | HTTP response envelope |
ResultReference |
§9 | Reference to a prior method's result |
Argument<T> |
§9 | A method argument that is either a value or a ResultReference |
Why
Client crates and server crates both need these types, but neither should pull in
the other's dependencies. jmap-types has exactly three dependencies — serde,
serde_json, and thiserror — and no async runtime, no HTTP framework, and no
application logic. Any crate in the jmap-* family can depend on it without cost.
Usage
Add to Cargo.toml:
[]
= "0.1"
Identifiers
use ;
// Construct from any string source
let id = from;
let date = from;
let state = from;
// Use as a string reference without allocating
println!; // "abc123"
let s: &str = id.as_ref; // borrow the inner string
// Consume to get the inner String
let raw: String = id.into_inner;
Building a request
use ;
use json;
let req = JmapRequest ;
let json = to_string?;
// {"using":[...],"methodCalls":[["Email/get",{"accountId":"u1","ids":["m1"]},"c1"]]}
Parsing a response
use JmapResponse;
let raw = r#"{
"methodResponses": [["Email/get", {"list": []}, "c1"]],
"sessionState": "75128aab4b1b"
}"#;
let resp: JmapResponse = from_str?;
println!; // "75128aab4b1b"
Method-level errors
use JmapError;
// Emit a typed error into a methodResponses array
let err = invalid_arguments;
let err = unknown_method;
let err = server_fail;
// alreadyExists requires the existing record's Id (RFC 8620 §5.4 MUST)
use Id;
let err = already_exists;
// Serialize for the wire — key is "type", not "error_type"
// {"type":"invalidArguments","description":"ids must not be empty"}
let json = to_string?;
Result references
use ;
// A plain value argument
let arg: = Value;
// A result reference — resolves at dispatch time
let rr = new;
let arg: = Ref;
How it works
Wire format
All types use serde with the JSON field names and structure RFC 8620 mandates:
JmapRequest.method_callsserializes as"methodCalls"(camelCase)JmapResponse.session_stateserializes as"sessionState"JmapError.error_typeserializes as"type"(the RFC field name)JmapError.existing_idserializes as"existingId"(present only foralreadyExists)Invocationis a type alias for(String, serde_json::Value, String), which serde serializes as a 3-element JSON array — exactly the RFC 8620 §3.2 format
Argument<T> and the sealed trait
Argument<T> uses #[serde(untagged)] to deserialize either as the value type T
or as a ResultReference (a JSON object). This works correctly only if T cannot
itself deserialize from a JSON object — otherwise serde would match T first and
silently swallow any ResultReference payload.
To prevent this, T is constrained to a sealed set of types that are guaranteed
to not deserialize from objects: String, Vec<String>, Id, Vec<Id>, u32,
u64, bool. serde_json::Value is deliberately excluded — Argument<Value>
fails to compile.
The sealed trait lives in a private module and cannot be implemented outside this crate. To add a new type, open a PR and verify the invariant holds.
#[non_exhaustive] and field privacy
Public structs are #[non_exhaustive] so that adding fields in future versions is
not a breaking change. The Id, UTCDate, and State newtypes also carry
#[non_exhaustive] specifically to prevent callers from pattern-matching the inner
field (e.g. let Id(s) = id;), which would otherwise lock in the single-field
layout as a semver guarantee. Use as_ref() or into_inner() instead.
What this crate does not do
- No validation:
Id::from("")succeeds. RFC 8620 requires non-empty Ids, but enforcement belongs in the consumer (e.g.jmap-server), not in the wire type. - No dispatch: method routing, argument resolution, and
#-prefixed key handling are the dispatcher's job. - No async: this crate has no runtime dependency.
Crate family
jmap-types ← this crate
├── jmap-server dispatcher, axum, tokio
├── jmap-mail-types RFC 8621 data types
└── jmap-chat-types Chat extension data types
References
- RFC 8620 — JMAP Core (normative)
- RFC 7807 — Problem Details for HTTP APIs (request-level errors, §3.6.1)
- RFC 6901 — JSON Pointer (used in
ResultReference.path)
License
Licensed under the MIT License. See LICENSE for details.