Parser Implementation Guide#
This guide will help you complete the JSON parser implementations throughout the JMAP codebase. All type definitions are complete - only the parsing logic needs to be filled in.
Overview#
Status: All of_json and to_json functions have stub implementations that raise "not yet implemented" errors.
Goal: Implement these functions using the provided test JSON files as specifications.
Tools: Use Jmap_parser.Helpers module for common parsing operations.
Implementation Strategy#
Step 1: Start with Primitives (Easiest)#
These are already complete, but review them as examples:
(* jmap-core/jmap_id.ml - COMPLETE *)
let of_json json =
match json with
| `String s -> of_string s
| _ -> raise (Jmap_error.Parse_error "Id must be a JSON string")
(* jmap-core/jmap_primitives.ml - COMPLETE *)
module UnsignedInt = struct
let of_json = function
| `Float f -> of_int (int_of_float f)
| `Int i -> if i >= 0 then of_int i else raise (Parse_error "...")
| _ -> raise (Parse_error "Expected number")
end
Step 2: Implement Core Parsers#
2.1 Comparator (Simple Object)#
File: jmap-core/jmap_comparator.ml
Test: test/data/core/request_query.json (sort field)
let of_json json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
let property = get_string "property" fields in
let is_ascending = get_bool_opt "isAscending" fields true in
let collation = get_string_opt "collation" fields in
{ property; is_ascending; collation }
2.2 Filter (Recursive Type)#
File: jmap-core/jmap_filter.ml
Test: test/data/core/request_query.json (filter field)
The generic of_json function is already complete. You need to implement condition parsers for each type (Mailbox, Email, etc.).
2.3 Session (Complex Nested Object)#
File: jmap-core/jmap_session.ml
Test: test/data/core/session.json
(* Account parser *)
module Account = struct
let of_json json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
{
name = get_string "name" fields;
is_personal = get_bool "isPersonal" fields;
is_read_only = get_bool "isReadOnly" fields;
account_capabilities = parse_map (fun v -> v)
(require_field "accountCapabilities" fields);
}
end
(* Session parser *)
let of_json json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
{
capabilities = parse_map (fun v -> v) (require_field "capabilities" fields);
accounts = parse_map Account.of_json (require_field "accounts" fields);
primary_accounts = parse_map expect_string (require_field "primaryAccounts" fields);
username = get_string "username" fields;
api_url = get_string "apiUrl" fields;
download_url = get_string "downloadUrl" fields;
upload_url = get_string "uploadUrl" fields;
event_source_url = get_string "eventSourceUrl" fields;
state = get_string "state" fields;
}
2.4 Invocation (3-tuple Array)#
File: jmap-core/jmap_invocation.ml
Test: Any request or response file (methodCalls/methodResponses field)
let of_json json =
let open Jmap_parser.Helpers in
match json with
| `A [method_name_json; arguments_json; call_id_json] ->
let method_name = expect_string method_name_json in
let call_id = expect_string call_id_json in
(* Parse based on method name *)
begin match witness_of_method_name method_name with
| Packed template ->
(* Parse arguments based on witness type *)
(* Return properly typed invocation *)
(* TODO: Complete this logic *)
raise (Parse_error "Invocation parsing not complete")
end
| _ -> raise (Parse_error "Invocation must be 3-element array")
2.5 Request and Response#
File: jmap-core/jmap_request.ml and jmap_response.ml
Test: All test/data/core/request_*.json and response_*.json
(* Request *)
let of_json json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
{
using = parse_array Jmap_capability.of_json
(require_field "using" fields);
method_calls = parse_array Jmap_invocation.of_json
(require_field "methodCalls" fields);
created_ids = match find_field "createdIds" fields with
| Some obj -> Some (parse_map Jmap_id.of_json obj)
| None -> None;
}
(* Response - similar pattern *)
Step 3: Implement Standard Method Parsers#
These follow predictable patterns. Example for Get:
File: jmap-core/jmap_standard_methods.ml
Tests: test/data/core/request_get.json, response_get.json
module Get = struct
let request_of_json parse_obj json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
{
account_id = Jmap_id.of_json (require_field "accountId" fields);
ids = parse_array_opt Jmap_id.of_json (find_field "ids" fields);
properties = parse_array_opt expect_string (find_field "properties" fields);
}
let response_of_json parse_obj json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
{
account_id = Jmap_id.of_json (require_field "accountId" fields);
state = get_string "state" fields;
list = parse_array parse_obj (require_field "list" fields);
not_found = parse_array Jmap_id.of_json (require_field "notFound" fields);
}
end
(* Repeat for Changes, Set, Copy, Query, QueryChanges *)
Step 4: Implement Mail Type Parsers#
4.1 Mailbox (Simple Mail Type)#
File: jmap-mail/jmap_mailbox.ml
Tests: test/data/mail/mailbox_get_response.json
module Rights = struct
let of_json json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
{
may_read_items = get_bool "mayReadItems" fields;
may_add_items = get_bool "mayAddItems" fields;
may_remove_items = get_bool "mayRemoveItems" fields;
may_set_seen = get_bool "maySetSeen" fields;
may_set_keywords = get_bool "maySetKeywords" fields;
may_create_child = get_bool "mayCreateChild" fields;
may_rename = get_bool "mayRename" fields;
may_delete = get_bool "mayDelete" fields;
may_submit = get_bool "maySubmit" fields;
}
end
let of_json json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
{
id = Jmap_id.of_json (require_field "id" fields);
name = get_string "name" fields;
parent_id = Option.map Jmap_id.of_json (find_field "parentId" fields);
role = get_string_opt "role" fields;
sort_order = Jmap_primitives.UnsignedInt.of_json
(require_field "sortOrder" fields);
total_emails = Jmap_primitives.UnsignedInt.of_json
(require_field "totalEmails" fields);
unread_emails = Jmap_primitives.UnsignedInt.of_json
(require_field "unreadEmails" fields);
total_threads = Jmap_primitives.UnsignedInt.of_json
(require_field "totalThreads" fields);
unread_threads = Jmap_primitives.UnsignedInt.of_json
(require_field "unreadThreads" fields);
my_rights = Rights.of_json (require_field "myRights" fields);
is_subscribed = get_bool "isSubscribed" fields;
}
4.2 Email (Most Complex)#
File: jmap-mail/jmap_email.ml
Tests:
test/data/mail/email_get_response.json(basic)test/data/mail/email_get_full_response.json(with body structure)
Start with submodules:
module EmailAddress = struct
let of_json json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
{
name = get_string_opt "name" fields;
email = get_string "email" fields;
}
end
module BodyPart = struct
(* Recursive parser for MIME structure *)
let rec of_json json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
{
part_id = get_string_opt "partId" fields;
blob_id = Option.map Jmap_id.of_json (find_field "blobId" fields);
size = Jmap_primitives.UnsignedInt.of_json (require_field "size" fields);
headers = parse_array parse_header (require_field "headers" fields);
name = get_string_opt "name" fields;
type_ = get_string "type" fields;
charset = get_string_opt "charset" fields;
disposition = get_string_opt "disposition" fields;
cid = get_string_opt "cid" fields;
language = parse_array_opt expect_string (find_field "language" fields);
location = get_string_opt "location" fields;
sub_parts = parse_array_opt of_json (find_field "subParts" fields);
}
and parse_header json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
(get_string "name" fields, get_string "value" fields)
end
(* Main Email parser *)
let of_json json =
let open Jmap_parser.Helpers in
let fields = expect_object json in
{
(* Parse all 24 fields *)
id = Jmap_id.of_json (require_field "id" fields);
blob_id = Jmap_id.of_json (require_field "blobId" fields);
thread_id = Jmap_id.of_json (require_field "threadId" fields);
mailbox_ids = parse_map (fun _ -> true) (require_field "mailboxIds" fields);
keywords = parse_map (fun _ -> true) (require_field "keywords" fields);
(* ... continue for all fields ... *)
from = parse_array_opt EmailAddress.of_json (find_field "from" fields);
to_ = parse_array_opt EmailAddress.of_json (find_field "to" fields);
body_structure = Option.map BodyPart.of_json (find_field "bodyStructure" fields);
(* ... etc ... *)
}
Step 5: Testing Pattern#
For each parser you implement:
(* In test/test_jmap.ml *)
let test_mailbox_parse () =
(* Load test JSON *)
let json = load_json "test/data/mail/mailbox_get_response.json" in
(* Parse response *)
let response = Jmap_mail.Jmap_mailbox.Get.response_of_json
Jmap_mailbox.Parser.of_json json in
(* Validate *)
check int "Mailbox count" 3 (List.length response.list);
let inbox = List.hd response.list in
check string "Inbox name" "Inbox" inbox.name;
check (option string) "Inbox role" (Some "inbox") inbox.role;
check bool "Can read" true inbox.my_rights.may_read_items;
let () =
run "JMAP" [
"Mailbox", [
test_case "Parse mailbox response" `Quick test_mailbox_parse;
];
]
Common Patterns#
Optional Fields#
(* Option with default *)
let is_ascending = get_bool_opt "isAscending" fields true
(* Option without default *)
let collation = get_string_opt "collation" fields
(* Map with option *)
parent_id = Option.map Jmap_id.of_json (find_field "parentId" fields)
Arrays#
(* Required array *)
ids = parse_array Jmap_id.of_json (require_field "ids" fields)
(* Optional array (null or array) *)
properties = parse_array_opt expect_string (find_field "properties" fields)
Maps (JSON Objects)#
(* String -> value *)
keywords = parse_map (fun v -> true) (require_field "keywords" fields)
(* Id -> value *)
mailbox_ids = parse_map Jmap_id.of_json (require_field "mailboxIds" fields)
Recursive Types#
(* Mutually recursive *)
let rec parse_filter parse_condition json =
match json with
| `O fields ->
match List.assoc_opt "operator" fields with
| Some op -> (* FilterOperator *)
let conditions = parse_array (parse_filter parse_condition) ... in
Operator (op, conditions)
| None -> (* FilterCondition *)
Condition (parse_condition json)
| _ -> raise (Parse_error "...")
Helper Reference#
(* From Jmap_parser.Helpers *)
(* Type expectations *)
expect_object : json -> (string * json) list
expect_array : json -> json list
expect_string : json -> string
expect_int : json -> int
expect_bool : json -> bool
(* Field access *)
find_field : string -> fields -> json option
require_field : string -> fields -> json
(* Typed getters *)
get_string : string -> fields -> string
get_string_opt : string -> fields -> string option
get_bool : string -> fields -> bool
get_bool_opt : string -> fields -> bool -> bool (* with default *)
get_int : string -> fields -> int
get_int_opt : string -> fields -> int option
(* Parsers *)
parse_map : (json -> 'a) -> json -> (string * 'a) list
parse_array : (json -> 'a) -> json -> 'a list
parse_array_opt : (json -> 'a) -> json option -> 'a list option
Order of Implementation#
Recommended order (easiest to hardest):
- ✅ Primitives (already done)
Jmap_comparator- Simple objectJmap_capability.CoreCapability- Nested objectJmap_session- Complex nested object with mapsJmap_standard_methods.Get.request- Simple with optionalsJmap_standard_methods.Get.response- With generic list- Other standard methods (Changes, Query, etc.)
Jmap_invocation- Array tuple with GADT dispatchJmap_requestandJmap_response- Top-level protocolJmap_mailbox- Simplest mail typeJmap_thread- Very simple (2 fields)Jmap_identity- Medium complexityJmap_vacation_response- Singleton patternJmap_search_snippet- Search resultsJmap_email_submission- With enums and envelopeJmap_email- Most complex (save for last)
Validation Strategy#
For each parser:
- Parse test file: Ensure no exceptions
- Check required fields: Verify non-optional fields are present
- Validate values: Check actual values match test file
- Round-trip: Serialize and parse again, compare
- Error cases: Try malformed JSON, missing fields
Serialization (to_json)#
After parsing is complete, implement serialization:
let to_json t =
`O [
("id", Jmap_id.to_json t.id);
("name", `String t.name);
("sortOrder", Jmap_primitives.UnsignedInt.to_json t.sort_order);
(* ... *)
]
Remove fields that are None:
let fields = [
("id", Jmap_id.to_json t.id);
("name", `String t.name);
] in
let fields = match t.parent_id with
| Some pid -> ("parentId", Jmap_id.to_json pid) :: fields
| None -> fields
in
`O fields
Common Pitfalls#
-
Case sensitivity: JSON field names are case-sensitive
- Use
"receivedAt"not"receivedat"
- Use
-
Null vs absent: Distinguish between
nulland field not present| Some `Null -> None (* null *) | Some value -> Some (parse value) (* present *) | None -> None (* absent *) -
Empty arrays:
[]is different fromnullparse_array_opt (* Returns None for null, Some [] for [] *) -
Number types: JSON doesn't distinguish int/float
| `Float f -> int_of_float f | `Int i -> i -
Boolean maps: Many fields are
Id[Boolean]mailbox_ids = parse_map (fun _ -> true) field
Getting Help#
- Check test files: They contain the exact JSON structure
- Look at existing parsers: Id and primitives are complete
- Use the helpers: They handle most common cases
- Follow the types: Type errors will guide you
Success Criteria#
Parser implementation is complete when:
- All test files parse without errors
- All required fields are extracted
- Optional fields handled correctly
- Round-trip works (parse -> serialize -> parse)
- All 50 test files pass
- No TODO comments remain in parser code
Good luck! Start simple and build up to the complex types. The type system will guide you.