neiam
/apollos_crib
It's Apollo's Crib
public · Issues · Pulls 1 · Labels · Forks · Compare · Actions failure · Badges · Packages
⭐
Log in to mark this repository.
JSON: keep the key order Poison wrote
6696166 · Gabriel Morell · 2026-10-23 13:11
Message
{commit_body(@commit)}
Files changed
modified
apps/room_sanctum/lib/room_sanctum/json.ex
+12
−2
@@ -23,7 +23,8 @@ defmodule RoomSanctum.JSON do
| 23 | 23 | |
| 24 | 24 | @doc """ |
| 25 | 25 | `term` with everything Jason would refuse made into what Poison wrote |
| 26 | − for it. Dates and times are left alone: both write them as ISO 8601. | |
| 26 | + for it, and every map made a `Jason.OrderedObject` in the key order | |
| 27 | + Poison wrote. Dates and times are left alone: both write them as ISO 8601. | |
| 27 | 28 | """ |
| 28 | 29 | def normalize(%struct{} = value) when struct in [Date, Time, NaiveDateTime, DateTime], do: value |
| 29 | 30 | def normalize(%Jason.Fragment{} = value), do: value |
@@ -32,7 +33,16 @@ defmodule RoomSanctum.JSON do
| 32 | 33 | def normalize(%Range{} = value), do: Enum.to_list(value) |
| 33 | 34 | def normalize(%MapSet{} = value), do: value |> MapSet.to_list() |> normalize() |
| 34 | 35 | def normalize(%_{} = value), do: value |> Map.from_struct() |> normalize() |
| 35 | − def normalize(value) when is_map(value), do: Map.new(value, fn {k, v} -> {k, normalize(v)} end) | |
| 36 | + # Poison wrote an object's keys in the reverse of the map's own order (it | |
| 37 | + # folds over `Map.keys/1`, prepending), and Jason writes them in that order. | |
| 38 | + # A client that reads a board in the order it arrived -- the Android app | |
| 39 | + # lays its cards out that way -- would see them flip, so the order Poison | |
| 40 | + # wrote is kept. | |
| 41 | + def normalize(value) when is_map(value) do | |
| 42 | + values = :lists.foldl(&[{&1, normalize(Map.fetch!(value, &1))} | &2], [], Map.keys(value)) | |
| 43 | + %Jason.OrderedObject{values: values} | |
| 44 | + end | |
| 45 | + | |
| 36 | 46 | def normalize(value) when is_list(value), do: Enum.map(value, &normalize/1) |
| 37 | 47 | def normalize(value) when is_tuple(value), do: value |> Tuple.to_list() |> normalize() |
| 38 | 48 | def normalize(value), do: value |
modified
apps/room_sanctum/test/room_sanctum/json_test.exs
+15
−2
@@ -30,12 +30,25 @@ defmodule RoomSanctum.JSONTest do
| 30 | 30 | end |
| 31 | 31 | |
| 32 | 32 | test "ranges and sets become lists, a URI its string" do |
| 33 | − assert JSON.encode!(%{r: 1..3, u: URI.parse("https://example.com/x")}) == | |
| 34 | − ~s({"r":[1,2,3],"u":"https://example.com/x"}) | |
| 33 | + # String keys: a small map orders them, so the order written is fixed. | |
| 34 | + assert JSON.encode!(%{"r" => 1..3, "u" => URI.parse("https://example.com/x")}) == | |
| 35 | + ~s({"u":"https://example.com/x","r":[1,2,3]}) | |
| 35 | 36 | |
| 36 | 37 | assert JSON.encode!(MapSet.new([{1, 2}])) == ~s([[1,2]]) |
| 37 | 38 | end |
| 38 | 39 | |
| 40 | + test "an object's keys come out in the order Poison wrote them" do | |
| 41 | + # The reverse of the map's own order. The Android app lays a board's | |
| 42 | + # cards out in the order the keys arrive, so this is what it shows. | |
| 43 | + board = %{ | |
| 44 | + "ephem-1" => %{"data" => [1], "query" => %{"name" => "Sky"}}, | |
| 45 | + "gtfs-2" => [%{"a" => 1, "b" => 2}] | |
| 46 | + } | |
| 47 | + | |
| 48 | + assert JSON.encode!(board) == | |
| 49 | + ~s({"gtfs-2":[{"b":2,"a":1}],"ephem-1":{"query":{"name":"Sky"},"data":[1]}}) | |
| 50 | + end | |
| 51 | + | |
| 39 | 52 | test "pretty printing still works" do |
| 40 | 53 | assert JSON.encode!(%{a: 1}, pretty: true) == ~s({\n "a": 1\n}) |
| 41 | 54 | end |
Parents: 819251f