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.
Numbers and valid UTF-8 strings are preserved. Other terms and invalid binaries
become inspected text; improper list tails become a final array value. Preserving
native types matters: a version number that arrives as 1 should not reach a
renderer as "1".
normalize/1 rejects collisions between converted map keys. The legacy
stringify/1 function remains lossy: string keys take precedence over other
keys that normalize to the same text. Use normalize/1 at input boundaries.
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.
Normalizes metadata, returning an error when converted map keys collide.
Recursively coerces a term into a JSON-encodable value with string keys.
Checks that a value contains only native JSON values and UTF-8 string keys.
Functions
@spec encode_struct(struct(), Jason.Encode.opts()) :: iodata()
Encodes a presentation struct as a plain string-keyed JSON object.
Presentation structs use this helper in their Jason.Encoder implementations
to emit all fields with string keys. This is distinct from stringify/1,
which treats arbitrary structs as textual metadata values.
Normalizes metadata, returning an error when converted map keys collide.
Recursively coerces a term into a JSON-encodable value with string keys.
Checks that a value contains only native JSON values and UTF-8 string keys.