Generate type safe json decoders and encoders from OAS

Hey!
I wanted to share a thing I created and get feedback on the idea as well. :slight_smile:

Problem in rough terms - as far as I can tell, traditionally generated Elm SDKs have used records for API inputs and outputs, but some languages run servers side often aren’t great about even acknowledging that non-nullable types exist. Nullability is kinda weird in OAS as well, so you might end up generating records where very field is a Maybe. This is especially bad when interacting with large legacy systems.
But if a spec exists, I still want to get guarantees at least about the stuff that’s in the spec.
Also something being “required” in the spec doesn’t mean it’s not null, just that the field is required.
For encoding I may want to omit required fields and rely on BE validation to tell me what to include.

Solution - generate the rough shape of a JSON schema as a type alias like:

type alias Profile =
    { obj :
        { username : Required { str : Sdk.Key.Supported }
        , bio : { str : Sdk.Key.Supported }
        , image : Required { str : Sdk.Key.Supported }
        , following : Required { bool : Sdk.Key.Supported }
        }
    }

And require that shape in the decoder and encoder types.

createArticle : Cmd Msg
createArticle =
  let
    input : Encode.Value Sdk.NewArticleRequest
    input =
      Encode.object [
        Encode.pair Sdk.article <| Encode.object
          [ Encode.pair Sdk.title <| Encode.string articleTitle
          , Encode.pair Sdk.description <| Encode.string articleDescription
          , Encode.pair Sdk.body <| Encode.string articleBody
          , Encode.pair Sdk.tagList <| Encode.list Encode.string articleTags
          ]
      ]

    decoder : Decode.Decoder Sdk.SingleArticleResponse Article
    decoder =
      Decode.field Sdk.article articleDecoder
  in
    Sdk.createArticle
      { body = Sdk.jsonBody input
      , expect = Sdk.expectJson ArticleCreateReceived decoder
      }

I have included the generated code in the repository as well for easier browsing.

1 Like

Since this is a request for feedback, I didn’t want it to go unanswered in the shadow of all the excitement over elm-run. If this doesn’t get me banned from here, I’m going to let Claude Code answer from a session inside my own code base that explores the same domain: elm-codegen for type-safe encoders/decoders… but where I own the server, its consumers, and the grammar. For specs you don’t control, a phantom-typed layer like yours is the right tool.


The all-Maybe blowup from servers that won’t commit to non-nullability is real, and I like that your answer isn’t “generate stricter records” but “generate the shape and let me hand-write a partial decoder that still type-checks against it.” That keeps the guarantee you actually want — about the fields that are in the spec — without pretending the server is honest about the rest. The field tokens as phantom carriers (username : Key { fields | username : a } a) accumulating the shape through pair/object and then checked against the generated alias is a genuinely different, and nice, answer.

Where I’d push is your sharpest point — required ≠ not null. That’s the part the code currently stubs: NullableType and UnionType both resolve to Never, and on decode optional key dec "" collapses absent, null, and an actual empty string into one value — so the very distinction you’re calling out gets erased right where you’d want it. OAS is genuinely murky about nullability, agreed, but the generated layer is exactly where you can recover it: a nullable marker carried in the shape as a sibling to Required, plus a nullable combinator that hands you a Maybe a only for keys the schema marks nullable, would let “required” and “nullable” be the two independent axes they actually are — and it looks like it’d fall straight out of the phantom machinery you’ve already built.

After that comes the part I think is the real prize. What the whole space is missing — not just Elm — is a codec that’s flexible and partial and compile-time-checked and sum-aware at the same time. Runtime validators like zod and io-ts do unions, but the check runs at runtime; the heavier type-level approaches manage it at a real ergonomic cost; TS’s structural unions aren’t really sums. oneOf/anyOf are Never here today — but modelling a oneOf as a phantom sum, encoding which variant at the type level while keeping the partial, hand-written, shape-checked decoding you’ve already got, is the combination nobody has landed cleanly.

That’s where this stops being a neat Elm experiment and becomes something the wider space genuinely doesn’t have.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.