Conversation
Create packages/quicktype-core/src/language/Gleam/ with index, language, GleamRenderer, constants, and utils modules mirroring the Crystal sibling. Register GleamTargetLanguage in All.ts and re-export from language/index.ts. `node dist/index.js --lang gleam` now emits a Gleam module for trivial JSON input, and `-o Foo.gleam` extension inference resolves to the language.
Populate constants.ts with Gleam's reserved words (keywords used today plus identifiers reserved for future use) and the imported module aliases. Add snake_case and PascalCase naming functions in utils.ts and wire the four namers plus forbiddenNamesForGlobalNamespace into GleamRenderer.
Emit record types, enum types, tagged unions, the recursive JsonValue type for `any`, and one `<name>_to_json(x) -> json.Json` encoder per type. Optional and nullable properties collapse to `option.Option`, encoded by omitting the key on `None`. Declare `supportsOptionalClassProperties` and `supportsUnionsWithBothNumberTypes` on the target language. To satisfy `gleam format --check`, add a small Wadler-style pretty-printer (pretty.ts) that reproduces the formatter's exact 80-column layout, including list-spread breaking, the `json.object([...])` hug, and the trailing-lambda break with its one-column slack. Reserve Gleam prelude type and constructor names and the fixed JsonValue identifiers, and force identifiers to begin with a letter. Verified: 53/54 priority+sample JSON inputs and all 88 schema inputs build with `gleam build` and pass `gleam format --check`. The lone exception, blns-object, hits a gleam Erlang-backend bug on an adversarial key and is a step 6 skip candidate.
Emit one `<name>_decoder() -> decode.Decoder(T)` per type using the `gleam/dynamic/decode` API. Every decoder opens with `use <- decode.recursive` so a self-referencing decoder cannot loop at construction; a later commit narrows the guard to cycle breakers. Optional-or-null fields use `decode.optional_field` with `decode.optional`, and the `JsonValue` catch-all is emitted last in `decode.one_of`. `number` fields accept an int and widen it, since Erlang's int and float decoders are disjoint. Extend the pretty-printer to reproduce `gleam format`'s hug rules for constructor calls, trailing lambdas, and `one_of`. Add unit tests for the `decode.recursive` guard and the `json_value_decoder` catch-all ordering.
Add test/fixtures/gleam/: a `main`-named Gleam project whose driver reads a JSON file path from argv, decodes it into TopLevel with the generated decoder, re-encodes to stdout, and exits non-zero on decode failure (the whole assertion for `.fail.json` samples). Commit manifest.toml with pinned dependency versions so a Hex release cannot break an unrelated PR, and gitignore the build/ directory. Verified end-to-end against a copied fixture project: `gleam deps download`, `gleam build`, `gleam format --check src/`, a round-trip run that reproduces pokedex, and a non-zero exit on an expected-failure sample.
Register GleamLanguage in test/languages.ts and add JSONFixture and JSONSchemaFixture entries in test/fixtures.ts (name "gleam" / "schema-gleam"). Work around a gleam-compiler flake: its BEAM backend segfaults at a low rate when compiling large generated modules, much more often when many fixture workers compile in parallel. Precompile dependencies in setup against a committed placeholder src/quicktype.gleam, and serialize each per-sample `gleam build` with a shared flock, so module compilations never overlap. Skip lists, each justified inline: - skipJSON: keywords/nst-test-suite (module too large — gleam segfaults even single-process), blns-object (Erlang backend miscompiles a tab+emoji key), nbl-stats (gleam crashes on this module a few percent of the time even for an isolated build). - skipSchema: keyword-unions (~17k-line module the compiler segfaults on). - skipDiffViaSchema: bug427/github-events/recursive (JSON vs JSON-Schema paths name types/fields/enum cases differently; output is correct either way). Fix two decoder-strictness bugs surfaced by expected-failure samples: an optional (absent-allowed) non-nullable field must still reject an explicit null, and a property-less record must require a JSON object rather than accepting any value. Verified: `QUICKTEST=true FIXTURE=gleam,schema-gleam npm run test:fixtures` passes across four consecutive runs.
Add a gleam,schema-gleam row to the fixture matrix in test-pr.yaml and an "Install Gleam" step guarded by contains(matrix.fixture, 'gleam'), using erlef/setup-beam pinned to OTP 27 (required by gleam_json's OTP json backend) and gleam 1.14.0, mirroring the Elixir install step.
Add Gleam to the target-language table in README.md and to the supports line in packages/quicktype-vscode/README.md.
A record with no properties gets an encoder whose argument is never read, so Gleam warned about an unused `value`. The encoder now names that parameter `_value` when the record has no required or optional fields.
The Gleam compiler segfaults at a low rate on large generated modules under parallel fixture load. The compile command now retries `gleam build` up to five times inside the existing lock, since a fresh compile of the same module almost always succeeds.
Every emitted decoder used to open with `decode.recursive`, which re-runs the body on each decode. Now only classes and unions chosen as cycle breakers get the guard, and the JsonValue decoder keeps it because it is directly self-recursive. Enums and containers never need one.
Adds an opt-in `infersUnionsWithBothNumberTypes` flag on `TargetLanguage`. When set together with `supportsUnionsWithBothNumberTypes`, JSON inference keeps `[1, 1.5]` as `integer | double` instead of widening to `double`. Off by default; no language changes behavior yet.
Gleam opts into the new flag so mixed samples infer `Int | Float` rather than re-encoding `123` as `123.0`. Number unions try `decode.int` before strict `decode.float`, `gleam/int` is imported only when the lenient number decoder is used, and the fixture round-trips schema-path code instead of diffing it.
Not a tab-and-emoji key: a 280-char key makes a 270-char Erlang variable, past the 255-char atom cap. `gleam check` passes, so the fault is the Erlang backend. A 270-char synthetic key reproduces it. https://www.erlang.org/doc/system/system_limits.html
Deleted any superfluous code comment that re-described something obvious in the code itself. Comments recording external constraints stay.
There was a problem hiding this comment.
Hello! I'm the creator and lead maintainer of Gleam. Really cool to see this pull request, I've long thought it would be great to have Gleam support in Quicktype.
I've left one note inline about the code that generated which I think is important to resolve.
Rust syntax highlighting is the closest thing we have
GitHub has had Gleam syntax highlighting for 4-5 years now.
Int vs float consideration
Gleam is also the first quicktype target whose Int and Float are disjoint at runtime.
This is not true. Many of the supported languages do, including but not limited to Rust, Haskell, Python, Ruby, C, Elixir, Erlang, and Go.
Widening a whole number to a float is a wrong answer for Gleam, not a cosmetic one, and that shaped the one core change in this PR.
Why do you say this is the case? Rust goes much further than Gleam does in terms of splitting out its number types, so I'm surprised that using a float is considered an appropriate approach for Rust but not Gleam.
Gleam has two runtimes, Erlang and JavaScript, and each treats Int and Float differently. The types generated by this addition do not behave the same at runtime, but a round trip result will always match its source.
Generated decoders should take this into account as JSON does not have distinct integer and float types.
| private emitTopLevelAlias(t: Type, name: Name): void { | ||
| // `gleam format` always places an aliased type on its own line. | ||
| this.line(0, `pub type ${this.nameToString(name)} =`); | ||
| this.emitDoc(2, this.typeDoc(t)); | ||
| } |
There was a problem hiding this comment.
Use of aliases in this is considered an anti-pattern and is highly discouraged and may be deprecated in future. I think it is important that quicktype doesn't encode this pattern that we want to spread.
Instead in Gleam we would write the actual type without obscuring it with an alias. This is clearer and avoids the previously common problem of misunderstanding the semantics of aliases.
What changed
quicktype can now generate ✨Gleam! Given JSON samples or a JSON Schema, it emits custom types, encoders built on gleam/json, and decoders built on gleam/dynamic/decode that round-trip the input. Use --lang gleam from the CLI or gleam from the library.
Why
Gleam has
to_jsonanddecodegenerators for types written in Gleam, but it cannot generate types based on valid JSON or JSON Schema.Gleam is also the first quicktype target whose Int and Float are disjoint at runtime.
Per existing convention, quicktype will collapse heterogenous JSON numbers to the least precise version in the language. As an example, quicktype takes thise JSON:
[{"bar":123},{"bar":12.3}]And converts it into, for instance, this Rust:
Widening a whole number to a float is a wrong answer for Gleam, not a cosmetic one, and that shaped the one
corechange in this PR.When running the Gleam generator against that same JSON, we are able to produce this:
How it works
Two decisions a reviewer might not udnerstand from just the diff:
Recursion. A decoder that reaches itself has to open with
decode.recursive, or the generated module compiles clean and then hangs forever when the decoder is constructed. The guard re-runs the decoder body on every decode, so we emit it only where a cycle is broken:Numbers. A new
TargetLanguagehook,infersUnionsWithBothNumberTypes, lets a language ask inference to keep [1, 1.5] asInt | Floatinstead of collapsing it toFloat. It is off for every existing language, so their output does not change. Gleam turns it on, and a new priority sample,int-float-union.json, covers the case.What to check
Run.ts,Inference.ts, andInputs.tson the path every language takes. Runnpm run test:unitlocally and confirm in CI that no non-Gleam fixture produces different output.Risk
Gleam has two runtimes, Erlang and JavaScript, and each treats
IntandFloatdifferently. The types generated by this addition do not behave the same at runtime, but a round trip result will always match its source.Examples
Gist
AI Disclosure
Paired on this with Claude Opus 4.8, 5, and Fable 5.1.