Coerces arbitrary Elixir terms into something JSON can represent.
Documentation metadata is not JSON-shaped. The BEAM docs chunk hands back
atoms, charlists, tuples, and structs; Earmark node metadata carries atom
keys; frontmatter can hold anything YAML parses. All of it has to survive the
trip to a renderer, so stringify/1 walks a term and coerces what JSON
cannot express:
- atoms become strings — except
nil,true, andfalse, which JSON has natively and which would be useless as"nil" - tuples become lists, since JSON has no tuple
- map keys become strings, with anything exotic passed through
inspect/1rather than dropped - lists and maps are walked recursively
- structs become their
String.Charstext where they have one, and theirinspect/1form otherwise
Structs are not walked field by field, which would preserve more but is not
safe in general: a Regex carries a compiled re_pattern holding a
non-UTF-8 binary, and emitting that produces a map no JSON encoder can
encode. Losing the structure of an exotic value beats failing the build on
it.
Everything else — numbers, binaries — is left exactly as it is. Preserving
native types matters: a version number that arrives as 1 should not reach a
renderer as "1".
One implementation
Both the AST and ExDoc extractors go through this module. Two coercion passes
drifting apart would show up as one artifact spelling :since metadata
differently from another, and a renderer discovering it in production.
Examples
iex> DocShell.Json.stringify(%{since: "1.2.0", deprecated: nil})
%{"since" => "1.2.0", "deprecated" => nil}
iex> DocShell.Json.stringify({:ok, [:a, 1]})
["ok", ["a", 1]]
iex> DocShell.Json.stringify(%{released: ~D[2026-08-05]})
%{"released" => "2026-08-05"}
Summary
Functions
Encodes a presentation struct as a plain string-keyed JSON object.
Recursively coerces a term into a JSON-encodable value with string keys.
Functions
@spec encode_struct( struct(), Jason.Encode.opts() ) :: iodata()
Encodes a presentation struct as a plain string-keyed JSON object.
Deriving Jason.Encoder would serialise fields in defstruct order, whereas
these shapes were string-keyed maps and so encoded lexicographically. Keeping
string keys keeps that order, which keeps the artifact byte-stable across the
switch to structs — an artifact diff should show a content change or nothing.
Recursively coerces a term into a JSON-encodable value with string keys.