Phx.gen.json

I’ve run into a problem making a simple API leveraging mix phx.gen.json.

Steps to reproduce:

  1. Make a new phoenix application (mix phx.new sd)
  2. cd sd
  3. mix ecto.create
  4. mix phx.gen.json People Prospect prospects email:string:unique slot_score:integer sms_phone:string
  5. mix ecto.migrate
  6. uncomment API scope in router.ex
  7. add resources “/prospect”, ProspectController inside the “/api” scope block
  8. mix test (17 tests pass, 0 failures)
  9. mix phx.server (running on localhost:4000)
  10. using a http client (Postman) send
    POST http://localhost:4000/api/prospect?email=test@test.com&slot_score:=1&sms_phone=8015551212

The result is the following message in the console:

[info] POST /api/prospect
[debug] Processing with SdWeb.ProspectController.create/2
Parameters: %{“email” => "test@test.com", “slot_score:” => “1”, “sms_phone” => “8015551212”}
Pipelines: [:api]
[info] Sent 400 in 45ms
[debug] ** (Phoenix.ActionClauseError) no function clause matching in SdWeb.ProspectController.create/2

The following arguments were given to SdWeb.ProspectController.create/2:

# 1
%Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{}, before_send: [#Function<1.112466771/1 in Plug.Logger.call/2>, #Function<0.33752287/1 in Phoenix.LiveReloader.before_send_inject_reloader/2>], body_params: %{}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: "localhost", method: "POST", owner: #PID<0.575.0>, params: %{"email" => "test@test.com", "slot_score:" => "1", "sms_phone" => "8015551212"}, path_info: ["api", "prospect"], path_params: %{}, port: 4000, private: %{SdWeb.Router => {[], %{}}, :phoenix_action => :create, :phoenix_controller => SdWeb.ProspectController, :phoenix_endpoint => SdWeb.Endpoint, :phoenix_format => "json", :phoenix_layout => {SdWeb.LayoutView, :app}, :phoenix_pipelines => [:api], :phoenix_router => SdWeb.Router, :phoenix_view => SdWeb.ProspectView, :plug_session_fetch => #Function<1.58261320/1 in Plug.Session.fetch_session/1>}, query_params: %{"email" => "test@test.com", "slot_score:" => "1", "sms_phone" => "8015551212"}, query_string: "email=test@test.com&slot_score:=1&sms_phone=8015551212", remote_ip: {127, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{"accept", "*/*"}, {"accept-encoding", "gzip, deflate"}, {"cache-control", "no-cache"}, {"connection", "keep-alive"}, {"content-length", "0"}, {"cookie", "_scandimension_key=SFMyNTY.g3QAAAABbQAAAA9jdXJyZW50X3VzZXJfaWRhAQ.HORxU__-f-buE_aqcmnL1jb4AGUJ7qUL7ZJI-9JEUFY"}, {"host", "localhost:4000"}, {"postman-token", "f2ccbbee-93e7-4d2e-9fb5-2c0dbf6a3c60"}, {"user-agent", "PostmanRuntime/7.6.1"}], request_path: "/api/prospect", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "FZbzBCQZewyGPQoAAARD"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: nil}

# 2
%{"email" => "test@test.com", "slot_score:" => "1", "sms_phone" => "8015551212"}

(sd) lib/sd_web/controllers/prospect_controller.ex:14: SdWeb.ProspectController.create(%Plug.Conn{adapter: {Plug.Cowboy.Conn, :...}, assigns: %{}, before_send: [#Function<1.112466771/1 in Plug.Logger.call/2>, #Function<0.33752287/1 in Phoenix.LiveReloader.before_send_inject_reloader/2>], body_params: %{}, cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false, host: "localhost", method: "POST", owner: #PID<0.575.0>, params: %{"email" => "test@test.com", "slot_score:" => "1", "sms_phone" => "8015551212"}, path_info: ["api", "prospect"], path_params: %{}, port: 4000, private: %{SdWeb.Router => {[], %{}}, :phoenix_action => :create, :phoenix_controller => SdWeb.ProspectController, :phoenix_endpoint => SdWeb.Endpoint, :phoenix_format => "json", :phoenix_layout => {SdWeb.LayoutView, :app}, :phoenix_pipelines => [:api], :phoenix_router => SdWeb.Router, :phoenix_view => SdWeb.ProspectView, :plug_session_fetch => #Function<1.58261320/1 in Plug.Session.fetch_session/1>}, query_params: %{"email" => "test@test.com", "slot_score:" => "1", "sms_phone" => "8015551212"}, query_string: "email=test@test.com&slot_score:=1&sms_phone=8015551212", remote_ip: {127, 0, 0, 1}, req_cookies: %Plug.Conn.Unfetched{aspect: :cookies}, req_headers: [{"accept", "*/*"}, {"accept-encoding", "gzip, deflate"}, {"cache-control", "no-cache"}, {"connection", "keep-alive"}, {"content-length", "0"}, {"cookie", "_scandimension_key=SFMyNTY.g3QAAAABbQAAAA9jdXJyZW50X3VzZXJfaWRhAQ.HORxU__-f-buE_aqcmnL1jb4AGUJ7qUL7ZJI-9JEUFY"}, {"host", "localhost:4000"}, {"postman-token", "f2ccbbee-93e7-4d2e-9fb5-2c0dbf6a3c60"}, {"user-agent", "PostmanRuntime/7.6.1"}], request_path: "/api/prospect", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-request-id", "FZbzBCQZewyGPQoAAARD"}], scheme: :http, script_name: [], secret_key_base: :..., state: :unset, status: nil}, %{"email" => "test@test.com", "slot_score:" => "1", "sms_phone" => "8015551212"})
(sd) lib/sd_web/controllers/prospect_controller.ex:1: SdWeb.ProspectController.action/2
(sd) lib/sd_web/controllers/prospect_controller.ex:1: SdWeb.ProspectController.phoenix_controller_pipeline/2
(sd) lib/sd_web/endpoint.ex:1: SdWeb.Endpoint.instrument/4
(phoenix) lib/phoenix/router.ex:275: Phoenix.Router.__call__/1
(sd) lib/sd_web/endpoint.ex:1: SdWeb.Endpoint.plug_builder_call/2
(sd) lib/plug/debugger.ex:122: SdWeb.Endpoint."call (overridable 3)"/2
(sd) lib/sd_web/endpoint.ex:1: SdWeb.Endpoint.call/2
(phoenix) lib/phoenix/endpoint/cowboy2_handler.ex:33: Phoenix.Endpoint.Cowboy2Handler.init/2
(cowboy) /Users/chipcoons/Dev/Phoenix/sd/deps/cowboy/src/cowboy_handler.erl:41: :cowboy_handler.execute/2
(cowboy) /Users/chipcoons/Dev/Phoenix/sd/deps/cowboy/src/cowboy_stream_h.erl:296: :cowboy_stream_h.execute/3
(cowboy) /Users/chipcoons/Dev/Phoenix/sd/deps/cowboy/src/cowboy_stream_h.erl:274: :cowboy_stream_h.request_process/3
(stdlib) proc_lib.erl:249: :proc_lib.init_p_do_apply/3

I would think that the code from phx.gen.json has been used by a lot of the community, so am assuming I missed something simple and obvious, but am pulling my hair out trying to figure out what it is.

Any thoughts?

The error you’re getting is that there is no clause of create that matches the inputs it received. You sent data in the shape of %{"email" => "test@test.com", "slot_score:" => "1", "sms_phone" => "8015551212"}. Take a look at your create. Does the second parameter look a bit like %{"prospect" => prospect_params} or something?

So the create clause says the shape of the data coming in should be %{"prospect" => prospect_params}. I prefer not to have these wrapping keys, I generally use the plain data as the input for my endpoints, and it seems like that’s what you want too. Try rewriting your create so it looks something like def create(conn, params) do.

Your other option is wrapping your data when you send it in Postman. So send something like this (I don’t know if your code says prospect, I’m guessing from your phx.gen.json arguments

%{
  "prospect" => %{
    "email" => "test@test.com",
    "slot_score:" => "1",
    "sms_phone" => "8015551212"
  }
}
2 Likes

Thanks for the tip. It is definitely something in how I’ve structured the POST in postman.

I changed the message to:

POST /api/prospect?prospect= {email: test@test.com, slot_score: 1, sms_phone: “8015551212”} HTTP/1.1
Host: localhost:4000
cache-control: no-cache
Postman-Token: 6e096d4b-0c44-44ed-8544-3110286dce01

And now get a different error:

[info] POST /api/prospect
[debug] Processing with SdWeb.ProspectController.create/2
Parameters: %{“prospect” => " {email: test@test.com, slot_score: 1, sms_phone: “8015551212”}"}
Pipelines: [:api]
[info] Sent 400 in 15ms
[debug] ** (Ecto.CastError) expected params to be a :map, got: " {email: test@test.com, slot_score: 1, sms_phone: \"8015551212\"}"

You cannot use json in urls like that. You need to encode the parameters properly or submit the json encoded data in the request’s body instead of the url.

3 Likes

Even a JSON encoded message body was failing to match. My data structure is nested, so it looks like the default decoding (when I had the JSON in the body) was failing since it was not turning the prospect JSON into a structure, it was decoding the data as a prospect" and keeping the remaining json string (with the prospect elements) which could not fulfill the match.

I was able to work around this by manually parsing the key I needed with a call the Map.fect!(“email”).