Skip to content

Formatters & Code

JSON to Elixir Struct

Elixir structs with @enforce_keys and typespecs.

Runs in your browser
JSON · source
lines: 17chars: 261size: 261 B
Elixir struct · result
lines: 26chars: 600size: 600 B
live

Understanding JSON → Elixir

Structs, maps, and the @enforce_keys trick.

Why Elixir JSON is more "structure the result" than "deserialize into a class", and how typespecs add the type information the runtime doesn't carry.

Structs are tagged maps.

An Elixir struct is a special map with a fixed set of keys and a type tag (the module name). Define one with defstruct [:id, :name, :avatar] inside a module; the struct is then accessed as %User{id: 7, name: "Q"}. Unlike map keys, struct keys are checked at compile time — a typo on %User{naem: ...} is a compile-time error. Structs are the right home for JSON DTOs.

@enforce_keys for required fields.

By default, struct fields default to nil when not provided. To require fields, add @enforce_keys [:id, :name] above the defstruct. Construction without those keys raises an ArgumentError at runtime. The combination (@enforce_keys plus defstruct) gives you required vs optional declaratively. Tools that emit Elixir code should enforce-keys every field that was present in every sample.

Jason is the standard library.

Jason (by Michał Muskała, José Valim's recommendation) is the de-facto JSON library — fast, simple, takes maps in and out by default. The output of Jason.decode!(json) is a plain map with string keys, which then gets mapped into your struct. There's no "annotation" step — the structure is built piece by piece, often via pattern-matching or with helper functions.

A worked example.

From { "user_id": 7, "name": "Q", "avatar": null }: defmodule User do @enforce_keys [:user_id, :name] defstruct [:user_id, :name, :avatar] @type t :: %__MODULE__{ user_id: integer(), name: String.t(), avatar: String.t() | nil } def from_json(%{"user_id" => id, "name" => name} = m) do %__MODULE__{user_id: id, name: name, avatar: m["avatar"]} end end The from_json function pattern-matches on the required keys (raises if missing) and accesses the optional one with the map index. The typespec gives Dialyzer the type information.

Pattern-match construction

%{"user_id" => id, "name" => name} = m

Pattern match guarantees required keys before the body runs.

User.from_json(%{"user_id" => 7, "name" => "Q"})

= %User{user_id: 7, name: "Q", avatar: nil}

Typespecs and Dialyzer.

Elixir is dynamically typed at runtime, but typespecs ( @type t :: %__MODULE__{...}, @spec foo(integer()) :: String.t() ) give Dialyzer enough information to flag type errors statically. For DTOs this is basically free type-safety: declare the typespec alongside the struct, then mix dialyzer catches misuses. The codegen tool should emit both the struct and the typespec.

Atoms, strings, and the safety dance.

Elixir maps can have either atom keys (:user_id) or string keys ("user_id"). Structs always have atom keys. Jason decodes JSON to string-keyed maps by default. Some codebases convert string keys to atoms with Jason.decode!(json, keys: :atoms); this is dangerous on untrusted input because atoms are not garbage-collected and a malicious JSON full of unique keys can exhaust atom-table memory. Use :atoms! (only existing atoms) or stay string-keyed and map by hand. Tools that emit the from_json helper should assume string keys.

Ecto changesets for validation.

Beyond simple structs, Phoenix-flavoured projects use Ecto changesets — pipelines of cast, validate_required, validate_length, validate_format. The result is either a valid changeset (apply it to get the struct) or an invalid one carrying a list of errors. For data that's crossing a trust boundary (HTTP body, file upload, message queue), Ecto changesets are the idiomatic upgrade from plain from_json functions.

Frequently asked questions

Quick answers.

How are JSON types mapped to Elixir types?

Strings map to `String.t()`, numbers to `number()` or `float()`, booleans to `boolean()`, and nulls are handled as `nil`. Nested objects are converted into internal map definitions or separate struct placeholders.

Does this tool handle nested JSON objects?

Yes. It will generate typespecs for nested structures, though you may need to manually extract nested maps into their own named modules for better Elixir project organisation.

What is the purpose of @enforce_keys in the output?

The generator identifies keys present in your JSON and includes them in `@enforce_keys` to ensure the struct cannot be initialised without those specific attributes.

Is my data sent to a backend for processing?

No. All transformation from JSON to Elixir code happens locally within your browser using JavaScript. No data is transmitted over the network.

People also search for

Related tools

More in this room.

See all in Formatters & Code