jmap-types 0.1.0

Shared JMAP wire types (RFC 8620) for the jmap-* crate family
Documentation

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:

[dependencies]
jmap-types = "0.1"

Identifiers

use jmap_types::{Id, UTCDate, State};

// Construct from any string source
let id = Id::from("abc123");
let date = UTCDate::from("2014-10-30T06:12:00Z");
let state = State::from("75128aab4b1b");

// Use as a string reference without allocating
println!("{}", id);          // "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 jmap_types::{JmapRequest, Invocation};
use serde_json::json;

let req = JmapRequest {
    using: vec![
        "urn:ietf:params:jmap:core".into(),
        "urn:ietf:params:jmap:mail".into(),
    ],
    method_calls: vec![
        ("Email/get".into(), json!({"accountId": "u1", "ids": ["m1"]}), "c1".into()),
    ],
    created_ids: None,
};

let json = serde_json::to_string(&req)?;
// {"using":[...],"methodCalls":[["Email/get",{"accountId":"u1","ids":["m1"]},"c1"]]}

Parsing a response

use jmap_types::JmapResponse;

let raw = r#"{
  "methodResponses": [["Email/get", {"list": []}, "c1"]],
  "sessionState": "75128aab4b1b"
}"#;

let resp: JmapResponse = serde_json::from_str(raw)?;
println!("{}", resp.session_state); // "75128aab4b1b"

Method-level errors

use jmap_types::JmapError;

// Emit a typed error into a methodResponses array
let err = JmapError::invalid_arguments("ids must not be empty");
let err = JmapError::unknown_method();
let err = JmapError::server_fail("unexpected database error");

// alreadyExists requires the existing record's Id (RFC 8620 §5.4 MUST)
use jmap_types::Id;
let err = JmapError::already_exists(Id::from("existing-record-id"));

// Serialize for the wire — key is "type", not "error_type"
// {"type":"invalidArguments","description":"ids must not be empty"}
let json = serde_json::to_string(&err)?;

Result references

use jmap_types::{Argument, ResultReference};

// A plain value argument
let arg: Argument<Vec<jmap_types::Id>> = Argument::Value(vec![Id::from("m1")]);

// A result reference — resolves at dispatch time
let rr = ResultReference::new("c0", "Email/query", "/ids");
let arg: Argument<Vec<jmap_types::Id>> = Argument::Ref(rr);

How it works

Wire format

All types use serde with the JSON field names and structure RFC 8620 mandates:

  • JmapRequest.method_calls serializes as "methodCalls" (camelCase)
  • JmapResponse.session_state serializes as "sessionState"
  • JmapError.error_type serializes as "type" (the RFC field name)
  • JmapError.existing_id serializes as "existingId" (present only for alreadyExists)
  • Invocation is 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.