michalmuskala / jason

A blazing fast JSON parser and generator in pure Elixir.
Other
1.6k stars 170 forks source link

Custom atom encoder for specific atoms #119

Closed sezaru closed 3 years ago

sezaru commented 3 years ago

Hello,

My program receives some maps that can contain some atoms in some values, for example:

%{key: %{key: :my_atom}}

I want to convert this map to json but I want to first convert these atoms to integer by a converter map, so let's say in this case :my_atom needs to be converted to number 2.

Since I don't want to traverse the whole map twice (since Jason will already do that), is it possible to handle this via a Jason.Encoder ou via some other way inside the Jason encode! call?

I tried the following but without success:

defimpl Jason.Encoder, for: Atom do
  def encode(:my_atom, _), do: Jason.Encode.integer(2)
  def encode(atom, opts), do: Jason.Encode.atom(atom, opts)
end

Thanks

michalmuskala commented 3 years ago

It's not possible to override existing encoders. Additionally encoders for keys are completely different from regular encoders for values - in particular keys in objects in JSON are required to use string syntax, for example "2": .... instead of regular integer encoding 2: ....

A way around this, I could see, would be to define a wrapper data structure that would handle the encoding. Something along the lines of:

defmodule Wrapper do
  defstruct [:value]

  def new(map) when is_map(map), do: %__MODULE__{value: map}

  defimpl Jason.Encoder do
    import Jason.Encode, only: [value: 2, string: 2]

    def encode(%{value: map}, opts) do
      case Map.to_list(map) do
        [] -> 
          "{}"
        [{key, value} | rest] -> 
          ["{", key(key, opts), ":", value(value, opts), encode_loop(rest, opts), "}"]
      end
    end

    defp encode_loop([{key, value} | rest], opts) do
      [",", key(key, opts), ":", value(value, opts) | encode_loop(rest, opts)]
    end

    defp key(:my_atom, _), do: ~S|"2"|
    defp key(other, opts), do: string(to_string(other), opts)
  end
end

This does a lot of the handling manually, but does avoid double traversal of the data structures. At this point, it's possible to wrap the maps where you need this special handling with Wrapper.new(map) and pass that to Jason.encode!/2.

michalmuskala commented 3 years ago

I'm going to close this. If you have any follow-ups, please re-open.