Skip to Content

You write the request.jmapc writes the client.

jmapc is a compiler for JMAP. The request is the JSON the specification already defines; jmapc checks it against the specification and writes a client that can only call it correctly.

Read the documentationSource on GitHub

requests/ListInboxEmails.jmap.json
{ "methodCalls": [ ["Email/query", { "filter": {"inMailbox": "{{mailboxId}}"}, "limit": "{{limit}}" }, "search"], ["Email/get", { "#ids": {"resultOf": "search", "name": "Email/query", "path": "/ids"}, "properties": ["id", "subject", "from", "receivedAt"] }, "fetch"] ], "_returns": "fetch" }
jmapc generate
inbox.go
res, err := client.ListInboxEmails(ctx, c, client.ListInboxEmailsParams{ MailboxID: inbox, Limit: 25, }) if err != nil { return err } for _, email := range res.List { fmt.Println(email.ReceivedAt, email.From[0].Email, *email.Subject) }

A request that is wrong never reaches a server

Every request is read against the specification before a line of code is generated — as you type, when you build, and against a running server when you ask. A misspelling comes back with the name you meant.

requests/BadQuery.jmap.json: methodCalls[0].arguments.filter.hasAttachmnt: EmailFilterCondition has no property "hasAttachmnt" did you mean "hasAttachment"? requests/BadQuery.jmap.json: methodCalls[1].arguments.#ids.name: the referenced call is Email/query, but the reference names Email/get call "c0" invokes Email/query

Three languages, and almost nothing to install

The runtime is generated alongside the requests rather than shipped as a library, so what a project takes on is the generated directory and little else.

Go

The generated package uses the standard library and nothing outside it. Transmission is an http.Client you already have.

Rust

serde and serde_json, and no more: no HTTP stack, no TLS backend and no async runtime come with it. Transmission is a Transport you implement over whichever client the program already uses.

TypeScript

No dependencies at all. The only thing the platform has to provide is fetch.

A compiler, not a wrapper

These five all follow from that one difference.

No API of its own

The request file is JMAP. What you learn is the specification, and it is the same knowledge in all three languages.

A response shaped like the question

The result carries the properties the request asked for and nothing else. Ask for one more and the type grows; ask for one that does not exist and the build stops.

Errors at every level JMAP reports them

A failed request, a failed method call, a /set that answers 200 with a refusal — the generated code tells them apart so the caller does not have to.

Push and paging as loops, not plumbing

A request marked as watched follows the changes the server reports. One marked as paged reads a result the server returns a part at a time.

Tools around the edges

A command that sends a request to a real server, a JMAP server to test your own code against, and a schema file for the capabilities jmapc does not know yet.

Install

The client is generated with go generate, so record the tool in the module that uses it. A Rust or TypeScript project has no Go toolchain to run go tool with, and takes a binary from the releases instead.

go get -tool github.com/linyows/jmapc/cmd/jmapc

Write your first requestSee what is covered