Packages
fnord
0.9.10
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/safe_json.ex
defmodule SafeJson do
@moduledoc """
Thin wrapper around Jason that sanitizes binaries before encoding to prevent
`Jason.EncodeError` on invalid UTF-8. The JSON spec (RFC 8259) requires valid
Unicode strings, but subprocess output, CLI arguments, and external data can
contain arbitrary bytes. This module is the single chokepoint for all JSON
serialization in the app, so every path benefits from the same safety net.
Decode functions delegate directly to Jason today. Routing all decodes through
this module gives us a seam for future work: migrating to Elixir's stdlib
`JSON`, normalizing string/atom keys, etc.
## Struct serialization
Structs that need JSON encoding should implement the `SafeJson.Serialize`
protocol rather than using `@derive {Jason.Encoder, ...}`. This keeps all
serialization concerns inside SafeJson and avoids leaking the backend library
into consuming modules.
defimpl SafeJson.Serialize, for: MyStruct do
def for_json(%MyStruct{name: name, age: age}) do
%{name: name, age: age}
end
end
"""
# ---------------------------------------------------------------------------
# Encode
# ---------------------------------------------------------------------------
@spec encode(term) :: {:ok, String.t()} | {:error, Jason.EncodeError.t() | Exception.t()}
def encode(term), do: term |> sanitize() |> Jason.encode()
@spec encode(term, keyword) ::
{:ok, String.t()} | {:error, Jason.EncodeError.t() | Exception.t()}
def encode(term, opts), do: term |> sanitize() |> Jason.encode(opts)
@spec encode!(term) :: String.t()
def encode!(term), do: term |> sanitize() |> Jason.encode!()
@spec encode!(term, keyword) :: String.t()
def encode!(term, opts), do: term |> sanitize() |> Jason.encode!(opts)
# ---------------------------------------------------------------------------
# Decode - pass-through to Jason for now
# ---------------------------------------------------------------------------
defdelegate decode(input), to: Jason
defdelegate decode(input, opts), to: Jason
defdelegate decode!(input), to: Jason
defdelegate decode!(input, opts), to: Jason
# ---------------------------------------------------------------------------
# Sanitization
#
# Recursively walks the data structure and replaces invalid UTF-8 sequences
# in every binary value with the Unicode replacement character (U+FFFD).
#
# Structs that implement SafeJson.Serialize are converted to plain maps via
# for_json/1, then sanitized normally. Structs without the protocol are passed
# through unchanged -- Jason will raise if it can't encode them, which is the
# desired behavior (forces you to implement the protocol).
# ---------------------------------------------------------------------------
defp sanitize(value) when is_binary(value), do: String.replace_invalid(value)
defp sanitize(value) when is_struct(value) do
if SafeJson.Serialize.impl_for(value) do
value |> SafeJson.Serialize.for_json() |> sanitize()
else
value
end
end
defp sanitize(value) when is_map(value) do
Map.new(value, fn {k, v} -> {sanitize(k), sanitize(v)} end)
end
defp sanitize(value) when is_list(value), do: Enum.map(value, &sanitize/1)
defp sanitize(value) when is_tuple(value) do
value |> Tuple.to_list() |> Enum.map(&sanitize/1) |> List.to_tuple()
end
defp sanitize(value), do: value
end
defprotocol SafeJson.Serialize do
@moduledoc """
Protocol for converting structs to JSON-safe plain maps. Implement this
instead of `@derive {Jason.Encoder, ...}` to keep the JSON backend as an
internal detail of `SafeJson`.
"""
@doc """
Returns a plain map (or other JSON-encodable term) representing the struct.
The returned value will be recursively sanitized by SafeJson before encoding.
"""
@spec for_json(t) :: term
def for_json(value)
end