I’ve been working with some pretty large JSON objects, and the inherently unordered sorting caused by parsing a struct as a map makes it hard to find the keys that I am looking for when dealing with these large objects.
To resolve this, I figured out how to convert structs to JSON while preserving a desired sort order (e.g. alphabetical), by converting the structs to keyword lists before encoding them as JSON.
I’m dumping this info here in case it’s helpful for someone else. There may be errors that I am not aware of, but it has worked for me so far. (Please let me know if you find an issue!)
NOTE: The JSON spec (RFC 8259) defines the JSON key-value pairs as being unordered, but knowing this doesn’t make a large JSON object any easier for a person to read. This guide is intended to help the humans who have to read the JSON, not the machines.
Jason (Hex package)
Jason has a helper function to JSON-encode a keyword list. Here’s how to use it to encode a struct:
lib/your_project/some_struct.ex
defmodule YourProject.SomeStruct do
use Ecto.Schema
embedded_schema do
field :hello, :string
field :world, :string
end
defimpl Jason.Encoder do
@doc "Sort keys alphabetically."
def encode(struct, opts) do
struct
|> Map.from_struct()
|> Enum.into([])
|> Enum.sort()
|> Jason.Encode.keyword(opts)
end
end
end
Try it out in IEx:
iex(1)> %YourProject.SomeStruct{} |> Jason.encode!()
"{\"hello\":null,\"id\":null,\"world\":null}"
iex(2)> |> Jason.decode!()
%{"hello" => nil, "id" => nil, "world" => nil}
JSON (Native Elixir module in version >=1.18.0)
The newer native JSON Elixir module does not appear to have the same convenience function offered by Jason, so we need to define it ourselves (inspiration taken from JSON.Encoder.Map):
lib/your_project/json_helpers.ex
defmodule YourProject.JSONHelpers do
def encode_keyword_list!(value, encoder)
def encode_keyword_list!([], _encoder), do: "{}"
def encode_keyword_list!([{key, value}], encoder) do
[?{, key(key, encoder), ?:, encoder.(value, encoder), ?}]
end
def encode_keyword_list!([{key, value} | tail], encoder) do
[?{, key(key, encoder), ?:, encoder.(value, encoder) | next(tail, encoder)]
end
defp next([], _encoder), do: ~c"}"
defp next([{key, value} | tail], encoder) do
[?,, key(key, encoder), ?:, encoder.(value, encoder) | next(tail, encoder)]
end
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
end
Here’s the same struct from before, but tweaked to work with the native JSON module and our custom helper function:
lib/your_project/some_struct.ex
defmodule YourProject.SomeStruct do
use Ecto.Schema
embedded_schema do
field :hello, :string
field :world, :string
end
defimpl JSON.Encoder do
@doc "Sort keys alphabetically."
def encode(value, encoder) do
value
|> Map.from_struct()
|> Enum.into([])
|> Enum.sort()
|> YourProject.JSONHelpers.encode_keyword_list!(encoder)
end
end
end
Try it out in IEx:
iex(1)> %YourProject.SomeStruct{} |> JSON.encode!()
"{\"hello\":null,\"id\":null,\"world\":null}"
iex(2)> |> JSON.decode!()
%{"hello" => nil, "id" => nil, "world" => nil}
Hopefully this is helpful to someone else out there!




















