Stream: beginners

Topic: Parser for JSON with multiple possible shapes


view this post on Zulip Jonathan (Sep 07 2026 at 14:27):

I should preface this with the fact that I struggle to understand how the parser_for machinery and encodings work, and the indirection required to thread through the encodings and formats, and how parsing is made generic over the formats. Fortunately, (until now) I haven't had to, because the simple structural and nominal structs have mapped cleanly.

Here I have a case where the shape of the json can vary between multiple (2 for now) shapes, e.g., an error shape or a success shape, depending on the value of one of the fields. I.e. parsing into FallibleResponse(success_data, failure_data) := [Success(success_data), Failure(failure_data)], which by default has a different mapping to/from json.

It's not really clear to me from the docs how to achieve this, but I've got this far from just trying to satisfy the type checker before reaching a dead end:

FallibleResponse(success, failure) := [Success(success), Failure(failure)].{

    parser_for : encoding -> (state -> Try({ value : FallibleResponse(a, b), rest : state }, []))
        where [
            a.parser_for : encoding -> (_ -> Try({ value : a, rest : _ }, _)),
            b.parser_for : encoding -> (_ -> Try({ value : b, rest : _ }, _)),
            encoding.parse : Str -> Try(_out, _errs),
        ]
    parser_for = |encoding| |state| {

        # Here I tried seeing if this method of dispatching would accept using parser_for on Peek
        # parse_helper : encoding -> (state -> Try({ value : v, rest : state }, errs))
        #   where [v.parser_for : encoding -> (_ -> Try({ value : v, rest : _ }, _))]
        # parse_helper = |enc| |st| {
        #   V : v
        #   V.parser_for(enc)(st)
        # }

        Peek : { type : Str }
        peeked : { value : Peek, rest : state }
        peeked = Peek.parser_for(encoding)(state)? ## <== Error here

        if peeked.value.type == "error" {
            B : b
            parsed = B.parser_for(encoding)(state)?
            Ok({ value: FallibleResponse.Failure(parsed.value), rest: parsed.rest })
        } else {
            A : a
            parsed = A.parser_for(encoding)(state)?
            Ok({ value: FallibleResponse.Success(parsed.value), rest: parsed.rest })
        }
    }
}

The error I can't get past is This parser_for method is being called on a value whose type doesn't have that method. on the structural type { type : Str } aka Peek.

For now I'm going to do something simple and just test each of the variants outside of the parser_for and use parse errors as a cue to try the alternate.

view this post on Zulip Jonathan (Sep 07 2026 at 14:31):

If I make Peek a nominal type and opt in with parser_for : _, the error about the missing parser_for becomes a warning: This declaration has a type annotation but no implementation..

view this post on Zulip Jonathan (Sep 07 2026 at 14:36):

Just realised this is on a 6 day old compiler version. (I swapped back from main because there seemed to be some cache instabilities this morning I couldn't get to reproducing, plus multi-minute roc check)

view this post on Zulip Anton (Sep 07 2026 at 17:54):

Hi @Jonathan,
I'm not an expert on the encoding stuff but I did get something working with Claude. Is this what you want?

Final :: [].{}

## The wire shape a response is inspected as before choosing a variant.
Peek : { type : Str }

## A JSON response that is either a success payload or an error payload,
## discriminated by its `type` field.
FallibleResponse(success, failure) := [Success(success), Failure(failure)].{
    parser_for : _ -> (_ -> Try({ value : FallibleResponse(a, b), rest : _ }, [InvalidJson(Str), MissingRequiredField(Str), ..errs]))
        where [
            a.Json.Parseable([InvalidJson(Str), MissingRequiredField(Str), ..errs]),
            b.Json.Parseable([InvalidJson(Str), MissingRequiredField(Str), ..errs]),
        ]
    parser_for = |encoding| {
        Success : a
        Failure : b
        parse_success = Success.parser_for(encoding)
        parse_failure = Failure.parser_for(encoding)
        parse_peek = Peek.parser_for(encoding)

        |state| {
            peeked = parse_peek(state)?
            if peeked.value.type == "error" {
                parsed = parse_failure(state)?
                Ok({ value: FallibleResponse.Failure(parsed.value), rest: parsed.rest })
            } else {
                parsed = parse_success(state)?
                Ok({ value: FallibleResponse.Success(parsed.value), rest: parsed.rest })
            }
        }
    }

    is_eq : _
}

Resp : FallibleResponse({ data : Str }, { message : Str })

expect {
    result : Try(Resp, [InvalidJson(Str), MissingRequiredField(Str)])
    result = Json.parse("{\"type\":\"error\",\"message\":\"boom\"}")
    result == Ok(FallibleResponse.Failure({ message: "boom" }))
}

expect {
    result : Try(Resp, [InvalidJson(Str), MissingRequiredField(Str)])
    result = Json.parse("{\"type\":\"ok\",\"data\":\"hi\"}")
    result == Ok(FallibleResponse.Success({ data: "hi" }))
}

expect {
    result : Try(Resp, [InvalidJson(Str), MissingRequiredField(Str)])
    result = Json.parse("{\"data\":\"hi\"}")
    result == Err(MissingRequiredField("type"))
}

# Nested inside another derived shape, the custom parser is still picked up.
expect {
    result : Try({ responses : List(Resp) }, [InvalidJson(Str), MissingRequiredField(Str)])
    result = Json.parse("{\"responses\":[{\"type\":\"ok\",\"data\":\"a\"},{\"type\":\"error\",\"message\":\"b\"}]}")
    result == Ok({ responses: [FallibleResponse.Success({ data: "a" }), FallibleResponse.Failure({ message: "b" })] })
}

view this post on Zulip Jonathan (Sep 07 2026 at 21:23):

That looks like something I tried except Peek.parser_for didn't seem to be derived for me. Perhaps because Json.Parseable makes it all click. Thanks though, I'll give it a try, but I wouldn't know why this in particular works :sweat_smile:

view this post on Zulip Jonathan (Sep 07 2026 at 21:26):

On another note, I don't normally use Claude but I let it rip on the roclang repo, given the circumstances. Told me it couldn't be done - in general it's been interesting trying to get LLMs to understand the more funky static dispatch stuff.

view this post on Zulip Anton (Sep 08 2026 at 09:44):

I went with Fable 5.1 on xhigh for this question in case it helps

view this post on Zulip Luke Boswell (Sep 08 2026 at 09:52):

Wow, how is Fable 5.1 xHigh? I've never tried it

view this post on Zulip Anton (Sep 08 2026 at 11:30):

Excellent so far, I have not used it much yet though. For most of my Roc things I use opus xhigh.

view this post on Zulip Luke Boswell (Sep 08 2026 at 11:32):

I've been trending the other direction and I basically use Low thinking for everything

view this post on Zulip Luke Boswell (Sep 08 2026 at 11:33):

I'll bust out a higher thinking setting if I'm wanting some kind of analysis, like developing a planning document

view this post on Zulip Anton (Sep 08 2026 at 11:35):

Interesting, intuitively I would expect thinking to help for debugging but I have not done any comparative tests.

view this post on Zulip Luke Boswell (Sep 08 2026 at 11:36):

I feel like we've crossed a threshold with these frontier models where their baseline is so good we don't need the extended reasoning.

view this post on Zulip Luke Boswell (Sep 08 2026 at 11:37):

Like I've been doing it for months now and barely notice the difference

view this post on Zulip Anton (Sep 08 2026 at 11:43):

For agentic coding on Fable 5.1 it does make a significant difference:
image.png

view this post on Zulip Luke Boswell (Sep 08 2026 at 11:44):

I guess my feeling is that 65% is good enough


Last updated: Sep 24 2026 at 15:59 UTC