Advanced Parsing
This example parses a user profile from camelCase JSON, including fields that may be missing and a field with a default value. It also parses HTTP headers to show how Roc’s type-directed parsing works with formats other than JSON.
User := { user_name : Str, nickname ?: Str, email : Try(Str, [Missing]), role : [Restricted, Admin] ?? Restricted, }.{ parser_for : _ } parse_user : Str -> Try(User, [InvalidJson(Str), MissingRequiredField(Str)]) parse_user = Json.parser_camel() parse_headers : Str -> Try({ content_length : U64, x_auth_token : Try(Str, [Missing]) }, [BadHeader, MissingRequiredField(Str)]) parse_headers = Encoding.HttpHeader.parser_for() expect { user = parse_user( \\{"userName": "Sam"} )? user.user_name == "Sam" and user.email == Err(Missing) and (user.?nickname ?? "anonymous") == "anonymous" and user.role == Restricted } expect { user = parse_user( \\{"userName": "Sam", "email": "sam@example.com"} )? user.email == Ok("sam@example.com") } expect { user = parse_user( \\{"userName": "Sam", "nickname": "Sammy"} )? user.?nickname == Ok("Sammy") } expect { user = parse_user( \\{"userName": "Sam", "role": "Admin"} )? user.role == Admin } expect parse_user("{}").is_err() expect { headers = parse_headers("Content-Length: 42\r\n\r\n")? headers.content_length == 42 and headers.x_auth_token == Err(Missing) }
The user profile demonstrates three ways to handle a missing field:
nickname ?: Strdeclares an optional field. Reading it with.?nicknamereturns aTry, and??lets us supply a default when it is absent.email : Try(Str, [Missing])represents absence explicitly asErr(Missing).role : [Restricted, Admin] ?? Restrictedgives the field a default value. Whenroleis missing from the JSON, the parser fills inRestricted; an explicit"role": "Admin"overrides that default. The resulting record always has this field, so we can read it directly with.role.
Fields such as user_name remain required. Unlike the fallback in user.?nickname ?? "anonymous", which only supplies a value when reading the optional field, the role default becomes part of the parsed record.
Field defaults belong to nominal records, so User uses := rather than the type-alias syntax :. Nominal types also need to opt into derived parsing: parser_for : _ asks Roc to generate the parser for this record.
parser_camel() connects the JSON field userName to Roc’s user_name. Because we define the parser at the top level, it is assembled at compile time and is ready to parse input at runtime.
The HTTP header example uses the same approach: we describe the record we want, and the format’s parser fills it in.
Output
Run this from the directory that has main.roc in it:
$ roc test main.roc All (6) tests passed in 101.4 ms.
Validated with Roc nightly-2026-10-29-a0a9fc2.