JSON Without the Boilerplate: Luma Learns to Speak Data

JSON Without the Boilerplate: Luma Learns to Speak Data

February 9, 2026·
Luma Core Team

Every modern program eventually talks to the outside world. It reads a configuration file. It calls an API. It saves state between runs. And almost always, that conversation happens in JSON.

Most languages make this work, but few make it feel natural. You find yourself writing serializers, defining intermediate types, or importing libraries just to turn {"name": "Alice"} into something your program can actually use. It’s the kind of task that should take one line but somehow takes ten.

Luma now handles JSON natively - and it does so the way you’d expect if you designed it from scratch.

Three functions, no setup

The entire JSON story in Luma fits into three calls:

data: any = json.decode("{\"name\": \"Alice\", \"age\": 30}")
print(data["name"])
print(data["age"])
Alice
30

That’s it. No imports, no type annotations to satisfy a compiler, no intermediate objects. You hand json.decode a string, and you get back a value you can work with immediately - maps become maps, lists become lists, numbers become numbers.

Going the other direction is just as simple:

output: str = json.encode({"hello": "world", "nums": [1, 2, 3]})
print(output)
{"hello":"world","nums":[1,2,3]}

And when you need something human-readable - for logging, debugging, or just seeing what you’re working with:

print(json.pretty({"formatted": true, "count": 42}))
{
  "count": 42,
  "formatted": true
}

No configuration flags. No indent parameters you always have to look up. Just the right default.

Values that feel like values

One thing we cared about is that decoded JSON should feel like native Luma data. If a JSON file says "age": 30, you should get the integer 30, not a floating-point 30.0 that you have to convert yourself.

This sounds obvious, but it’s a detail many languages get wrong. Luma normalizes numbers during decoding so that whole numbers come back as integers and fractional ones stay as floats. You don’t have to think about it. It just works the way you’d read it.

Maps, lists, strings, booleans, and null all map to their natural Luma equivalents. There’s no separate “JSON object” type to learn. Once decoded, the data is just data.

Round-trips that don’t lose anything

A common pattern in programs is to load some data, change a piece, and save it back. This should work without surprises:

original: {str: any} = {"key": "value", "list": [1, 2, 3]}
encoded: str = json.encode(original)
decoded: any = json.decode(encoded)
print(decoded["key"])
value

Encode to a string, decode it back, and the data is the same. There’s no special handling needed, no lossy conversions hiding in the middle. The functions compose the way you’d hope.

Designed for the web too

Luma already had a json() helper for web server responses - the kind that wraps your data in the right headers and status code. But until now, there was no clean way to read JSON coming in from a request body.

With this update, the Request type now includes a body field - the raw request body as a string. Combined with json.decode, handling incoming JSON becomes straightforward:

handler: fn(Request) -> Response = req -> {
    input: any = json.decode(req.body)
    name: str = input["name"]
    return json({"greeting": "Hello, " + name})
}

No manual byte-to-string conversion. No separate body parsers. The pieces connect naturally because they were designed to work together.

Keeping things where they belong

One of the quieter decisions behind this feature is where the JSON functions live. They’re namespaced under json - not global, not imported, not attached to strings. You write json.decode(...), json.encode(...), json.pretty(...), and the intent is immediately clear at every call site.

This follows the same pattern as server.start(). Built-in capabilities that belong to a domain get a namespace. Everything else stays out of the way. Your program only pays for what it uses - if you never touch JSON, none of the machinery is included.

A practical piece of a larger picture

JSON support might seem like a small addition, but it unlocks a lot. Configuration files become easy to load. API responses become easy to parse. Web handlers can finally receive structured data, not just send it. Testing gets simpler because you can serialize expected results and compare them as strings.

More importantly, it removes one of those invisible barriers that make a language feel like a toy. When you can read and write the format that the rest of the world uses, your programs stop being isolated experiments and start being real tools.

That’s what Luma is building toward - not a language with the most features, but one where the features that matter are done right.

Last updated on