My agentic slop goes here. Not intended for anyone else!

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):

  1. ✅ Primitives (already done)
  2. Jmap_comparator - Simple object
  3. Jmap_capability.CoreCapability - Nested object
  4. Jmap_session - Complex nested object with maps
  5. Jmap_standard_methods.Get.request - Simple with optionals
  6. Jmap_standard_methods.Get.response - With generic list
  7. Other standard methods (Changes, Query, etc.)
  8. Jmap_invocation - Array tuple with GADT dispatch
  9. Jmap_request and Jmap_response - Top-level protocol
  10. Jmap_mailbox - Simplest mail type
  11. Jmap_thread - Very simple (2 fields)
  12. Jmap_identity - Medium complexity
  13. Jmap_vacation_response - Singleton pattern
  14. Jmap_search_snippet - Search results
  15. Jmap_email_submission - With enums and envelope
  16. Jmap_email - Most complex (save for last)

Validation Strategy#

For each parser:

  1. Parse test file: Ensure no exceptions
  2. Check required fields: Verify non-optional fields are present
  3. Validate values: Check actual values match test file
  4. Round-trip: Serialize and parse again, compare
  5. 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#

  1. Case sensitivity: JSON field names are case-sensitive

    • Use "receivedAt" not "receivedat"
  2. Null vs absent: Distinguish between null and field not present

    | Some `Null -> None  (* null *)
    | Some value -> Some (parse value)  (* present *)
    | None -> None  (* absent *)
    
  3. Empty arrays: [] is different from null

    parse_array_opt  (* Returns None for null, Some [] for [] *)
    
  4. Number types: JSON doesn't distinguish int/float

    | `Float f -> int_of_float f
    | `Int i -> i
    
  5. Boolean maps: Many fields are Id[Boolean]

    mailbox_ids = parse_map (fun _ -> true) field
    

Getting Help#

  1. Check test files: They contain the exact JSON structure
  2. Look at existing parsers: Id and primitives are complete
  3. Use the helpers: They handle most common cases
  4. 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.