Search code examples
elixirphoenix-framework

Phoenix wraps JSON request in a map with "_json" key


I have some issues running tests on a Phoenix controller. What I want is to supply a list of maps to it, but if I do just that, I get Argument Error.

Test code

      params =
        [
          %{"id" => 1, "sum" => 6},
          %{"id" => 2, "sum" => 4}
        ]


      conn =
        conn
        |> put(Routes.stat_path(conn, :replace_all), params)

The error

** (ArgumentError) argument error
     code: |> put(Routes.stat_path(conn, :replace_all), params)
     stacktrace:
       (stdlib 4.0.1) :maps.from_list([%{"id" => 1, "sum" => 6}, %{"id" => 2, "sum" => 4}])
       (elixir 1.13.4) lib/enum.ex:1448: Enum.into_map/1
       (plug 1.14.0) lib/plug/adapters/test/conn.ex:161: Plug.Adapters.Test.Conn.body_or_params/4
       (plug 1.14.0) lib/plug/adapters/test/conn.ex:21: Plug.Adapters.Test.Conn.conn/4
       (phoenix 1.6.15) lib/phoenix/test/conn_test.ex:236: Phoenix.ConnTest.dispatch_endpoint/5
       (phoenix 1.6.15) lib/phoenix/test/conn_test.ex:225: Phoenix.ConnTest.dispatch/5
       test/myapp_web/controllers/stat_controller_test.exs:15: (test)

I decided to do a small change and encode JSON myself in the tests:

params =
        [
          %{"id" => 1, "sum" => 6},
          %{"id" => 2, "sum" => 4}
        ]
        |> Jason.encode!()

      conn =
        conn
        # this line is required for binary data
        |> put_req_header("content-type", "application/json")
        |> put(Routes.stat_path(conn, :replace_all), params)

What I get in the controller now is a bit strange. My data is wrapped into a map with _json key, so instead of the list (which I need) I get this map:

%{"_json" => [%{"id" => 1, "sum" => 6}, %{"id" => 2, "sum" => 4}]}

What I'm doing wrong and how to properly test a Phoenix controller in case I want to supply a list of maps into it?


Solution

  • The culprit is Plug.Parsers.JSON.

    JSON documents that aren’t maps (arrays, strings, numbers, etc) are parsed into a "_json" key to allow proper param merging.

    Quoting José from this answer:

    You cannot [avoid wrapping] because conn.params is a map. So if we set it to a list, other parts of plug will fail. But the list is stored as is under _json. You can also disable Plug.Parsers.JSON and roll your own parser.

    That said, for this particular request you are to roll on your own parsed.