|
| 1 | +defprotocol JSON.Encoder do |
| 2 | + @moduledoc """ |
| 3 | + A protocol for custom JSON encoding of data structures. |
| 4 | + """ |
| 5 | + |
| 6 | + @fallback_to_any true |
| 7 | + |
| 8 | + @doc """ |
| 9 | + A function invoked to encode the given term. |
| 10 | + """ |
| 11 | + def encode(term, encoder) |
| 12 | +end |
| 13 | + |
| 14 | +defimpl JSON.Encoder, for: Atom do |
| 15 | + def encode(value, encoder) do |
| 16 | + case value do |
| 17 | + nil -> "null" |
| 18 | + true -> "true" |
| 19 | + false -> "false" |
| 20 | + _ -> encoder.(Atom.to_string(value), encoder) |
| 21 | + end |
| 22 | + end |
| 23 | +end |
| 24 | + |
| 25 | +defimpl JSON.Encoder, for: BitString do |
| 26 | + def encode(value, _encoder) do |
| 27 | + :elixir_json.encode_binary(value) |
| 28 | + end |
| 29 | +end |
| 30 | + |
| 31 | +defimpl JSON.Encoder, for: List do |
| 32 | + def encode(value, encoder) do |
| 33 | + :elixir_json.encode_list(value, encoder) |
| 34 | + end |
| 35 | +end |
| 36 | + |
| 37 | +defimpl JSON.Encoder, for: Integer do |
| 38 | + def encode(value, _encoder) do |
| 39 | + :elixir_json.encode_integer(value) |
| 40 | + end |
| 41 | +end |
| 42 | + |
| 43 | +defimpl JSON.Encoder, for: Float do |
| 44 | + def encode(value, _encoder) do |
| 45 | + :elixir_json.encode_float(value) |
| 46 | + end |
| 47 | +end |
| 48 | + |
| 49 | +defimpl JSON.Encoder, for: Map do |
| 50 | + def encode(value, encoder) do |
| 51 | + :elixir_json.encode_map(value, encoder) |
| 52 | + end |
| 53 | +end |
| 54 | + |
| 55 | +defimpl JSON.Encoder, for: Any do |
| 56 | + defmacro __deriving__(module, _struct, opts) do |
| 57 | + fields = module |> Macro.struct_info!(__CALLER__) |> Enum.map(& &1.field) |
| 58 | + fields = fields_to_encode(fields, opts) |
| 59 | + vars = Macro.generate_arguments(length(fields), __MODULE__) |
| 60 | + kv = Enum.zip(fields, vars) |
| 61 | + |
| 62 | + {io, _prefix} = |
| 63 | + Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix -> |
| 64 | + key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:]) |
| 65 | + {[key, quote(do: encoder.(unquote(value), encoder))], ?,} |
| 66 | + end) |
| 67 | + |
| 68 | + io = if io == [], do: "{}", else: io ++ [?}] |
| 69 | + |
| 70 | + quote do |
| 71 | + defimpl JSON.Encoder, for: unquote(module) do |
| 72 | + def encode(%{unquote_splicing(kv)}, encoder) do |
| 73 | + unquote(io) |
| 74 | + end |
| 75 | + end |
| 76 | + end |
| 77 | + end |
| 78 | + |
| 79 | + defp fields_to_encode(fields, opts) do |
| 80 | + cond do |
| 81 | + only = Keyword.get(opts, :only) -> |
| 82 | + case only -- fields do |
| 83 | + [] -> |
| 84 | + only |
| 85 | + |
| 86 | + error_keys -> |
| 87 | + raise ArgumentError, |
| 88 | + "unknown struct fields #{inspect(error_keys)} specified in :only. Expected one of: " <> |
| 89 | + "#{inspect(fields -- [:__struct__])}" |
| 90 | + end |
| 91 | + |
| 92 | + except = Keyword.get(opts, :except) -> |
| 93 | + case except -- fields do |
| 94 | + [] -> |
| 95 | + fields -- [:__struct__ | except] |
| 96 | + |
| 97 | + error_keys -> |
| 98 | + raise ArgumentError, |
| 99 | + "unknown struct fields #{inspect(error_keys)} specified in :except. Expected one of: " <> |
| 100 | + "#{inspect(fields -- [:__struct__])}" |
| 101 | + end |
| 102 | + |
| 103 | + true -> |
| 104 | + fields -- [:__struct__] |
| 105 | + end |
| 106 | + end |
| 107 | + |
| 108 | + def encode(%_{} = struct, _encoder) do |
| 109 | + raise Protocol.UndefinedError, |
| 110 | + protocol: @protocol, |
| 111 | + value: struct, |
| 112 | + description: """ |
| 113 | + JSON.Encoder protocol must be explicitly implemented for structs |
| 114 | +
|
| 115 | + If you own the struct, you can derive the implementation specifying \ |
| 116 | + which fields should be encoded to JSON: |
| 117 | +
|
| 118 | + @derive {JSON.Encoder, only: [....]} |
| 119 | + defstruct ... |
| 120 | +
|
| 121 | + It is also possible to encode all fields, although this should be \ |
| 122 | + used carefully to avoid accidentally leaking private information \ |
| 123 | + when new fields are added: |
| 124 | +
|
| 125 | + @derive JSON.Encoder |
| 126 | + defstruct ... |
| 127 | +
|
| 128 | + Finally, if you don't own the struct you want to encode to JSON, \ |
| 129 | + you may use Protocol.derive/3 placed outside of any module: |
| 130 | +
|
| 131 | + Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...]) |
| 132 | + Protocol.derive(JSON.Encoder, NameOfTheStruct) |
| 133 | + """ |
| 134 | + end |
| 135 | + |
| 136 | + def encode(value, _encoder) do |
| 137 | + raise Protocol.UndefinedError, |
| 138 | + protocol: @protocol, |
| 139 | + value: value |
| 140 | + end |
| 141 | +end |
| 142 | + |
| 143 | +defmodule JSON do |
| 144 | + @moduledoc """ |
| 145 | + JSON encoding and decoding. |
| 146 | +
|
| 147 | + Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and |
| 148 | + [ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) |
| 149 | + standards. |
| 150 | +
|
| 151 | + ## Encoding |
| 152 | +
|
| 153 | + Elixir built-in data structures are encoded to JSON as follows: |
| 154 | +
|
| 155 | + | **Elixir** | **JSON** | |
| 156 | + |------------------------|----------| |
| 157 | + | `integer() \| float()` | Number | |
| 158 | + | `true \| false ` | Boolean | |
| 159 | + | `nil` | Null | |
| 160 | + | `binary()` | String | |
| 161 | + | `atom()` | String | |
| 162 | + | `list()` | Array | |
| 163 | + | `%{binary() => _}` | Object | |
| 164 | + | `%{atom() => _}` | Object | |
| 165 | + | `%{integer() => _}` | Object | |
| 166 | +
|
| 167 | + You may also implement the `JSON.Encoder` protocol for custom data structures. |
| 168 | +
|
| 169 | + ## Decoding |
| 170 | +
|
| 171 | + Elixir built-in data structures are decoded from JSON as follows: |
| 172 | +
|
| 173 | + | **JSON** | **Elixir** | |
| 174 | + |----------|------------------------| |
| 175 | + | Number | `integer() \| float()` | |
| 176 | + | Boolean | `true \| false` | |
| 177 | + | Null | `nil` | |
| 178 | + | String | `binary()` | |
| 179 | + | Object | `%{binary() => _}` | |
| 180 | +
|
| 181 | + """ |
| 182 | + |
| 183 | + @moduledoc since: "1.18.0" |
| 184 | + |
| 185 | + @doc ~S""" |
| 186 | + Encodes the given term to JSON as a binary. |
| 187 | +
|
| 188 | + The second argument is a function that is recursively |
| 189 | + invoked to encode a term. |
| 190 | +
|
| 191 | + ## Examples |
| 192 | +
|
| 193 | + iex> JSON.encode([123, "string", %{key: "value"}]) |
| 194 | + "[123,\"string\",{\"key\":\"value\"}]" |
| 195 | +
|
| 196 | + """ |
| 197 | + def encode(term, encoder \\ &encode_value/2) do |
| 198 | + IO.iodata_to_binary(encoder.(term, encoder)) |
| 199 | + end |
| 200 | + |
| 201 | + @doc ~S""" |
| 202 | + Encodes the given term to JSON as an iodata. |
| 203 | +
|
| 204 | + This is the most efficient format if the JSON is going to be |
| 205 | + used for IO purposes. |
| 206 | +
|
| 207 | + The second argument is a function that is recursively |
| 208 | + invoked to encode a term. |
| 209 | +
|
| 210 | + ## Examples |
| 211 | +
|
| 212 | + iex> data = JSON.encode_to_iodata([123, "string", %{key: "value"}]) |
| 213 | + iex> IO.iodata_to_binary(data) |
| 214 | + "[123,\"string\",{\"key\":\"value\"}]" |
| 215 | +
|
| 216 | + """ |
| 217 | + def encode_to_iodata(term, encoder \\ &encode_value/2) do |
| 218 | + encoder.(term, encoder) |
| 219 | + end |
| 220 | + |
| 221 | + @doc """ |
| 222 | + A shortcut for `encode/2` used for compatibility purposes. |
| 223 | +
|
| 224 | + If you are targetting the `JSON` module directly, do not use |
| 225 | + this function, use `JSON.encode/2` instead. This function will |
| 226 | + be deprecated in Elixir v1.22 |
| 227 | + """ |
| 228 | + @doc deprecated: "Use JSON.encode/2 instead" |
| 229 | + # TODO: Deprecate on Elixir v1.22 |
| 230 | + def encode!(term, encoder \\ &encode_value/2) do |
| 231 | + encode(term, encoder) |
| 232 | + end |
| 233 | + |
| 234 | + @doc """ |
| 235 | + A shortcut for `encode_to_iodata/2` used for compatibility purposes. |
| 236 | +
|
| 237 | + If you are targetting the `JSON` module directly, do not use |
| 238 | + this function, use `JSON.encode/2` instead. This function will |
| 239 | + be deprecated in Elixir v1.22 |
| 240 | + """ |
| 241 | + @doc deprecated: "Use JSON.encode_to_iodata/2 instead" |
| 242 | + # TODO: Deprecate on Elixir v1.22 |
| 243 | + def encode_to_iodata!(term, encoder \\ &encode_value/2) do |
| 244 | + encode_to_iodata(term, encoder) |
| 245 | + end |
| 246 | + |
| 247 | + @doc """ |
| 248 | + This is the default function used to recursively encode each value. |
| 249 | + """ |
| 250 | + def encode_value(value, encoder) when is_atom(value) do |
| 251 | + case value do |
| 252 | + nil -> "null" |
| 253 | + true -> "true" |
| 254 | + false -> "false" |
| 255 | + _ -> encoder.(Atom.to_string(value), encoder) |
| 256 | + end |
| 257 | + end |
| 258 | + |
| 259 | + def encode_value(value, _encoder) when is_binary(value), |
| 260 | + do: :elixir_json.encode_binary(value) |
| 261 | + |
| 262 | + def encode_value(value, _encoder) when is_integer(value), |
| 263 | + do: :elixir_json.encode_integer(value) |
| 264 | + |
| 265 | + def encode_value(value, _encoder) when is_float(value), |
| 266 | + do: :elixir_json.encode_float(value) |
| 267 | + |
| 268 | + def encode_value(value, encoder) when is_list(value), |
| 269 | + do: :elixir_json.encode_list(value, encoder) |
| 270 | + |
| 271 | + def encode_value(%{} = value, encoder) when not is_map_key(value, :__struct__), |
| 272 | + do: :elixir_json.encode_map(value, encoder) |
| 273 | + |
| 274 | + def encode_value(value, encoder), |
| 275 | + do: JSON.Encoder.encode(value, encoder) |
| 276 | +end |
0 commit comments