# `Snodo.JSONValue`
[🔗](https://github.com/joshrotenberg/snodo/blob/v0.2.0/lib/snodo/json_value.ex#L1)

The JSON value rule every result, content map, and metadata map must satisfy.

A JSON value here is `nil`, a boolean, a number, a string, a list of JSON
values, or a map whose keys are **strings** and whose values are JSON values.
Atom keys and atom values are rejected. The protocol carries these values
through untouched, so accepting atoms would mean guessing at encoding and
silently resolving collisions between `:name` and `"name"`.

Elixir domain layers usually return atom-keyed maps and structs, so a value
arriving from application code often needs converting first.
`encodable!/1` does that:

    iex> Snodo.JSONValue.encodable!(%{name: "jason", tags: [:json, :parser]})
    %{"name" => "jason", "tags" => ["json", "parser"]}

`Snodo.Result.structured/2`, `Snodo.Resource.json/3`, and the content builders
all validate with `valid?/1` and raise on anything else.

# `encodable!`

```elixir
@spec encodable!(term()) :: term()
```

Converts an Elixir term into a JSON value, raising when it cannot.

The rules are fixed so the result is predictable:

  * atom keys become strings, and a map that would collide two keys onto one
    string raises rather than dropping either;
  * atom values become strings, except `nil`, `true`, and `false`;
  * `Date`, `Time`, `DateTime`, `NaiveDateTime`, `URI`, and `Version` become
    their canonical string forms;
  * any other struct becomes a map of its fields;
  * tuples, PIDs, references, functions, and ports raise, because there is no
    correct JSON form to pick for them. A keyword list is a list of tuples
    and therefore raises; convert it with `Map.new/1` first.

Strings are returned unchanged and are not checked for UTF-8 validity;
encoding catches that later.

# `valid?`

```elixir
@spec valid?(term()) :: boolean()
```

Returns whether a term is a JSON value under the rule above.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
