In Phoenix how does this work: %{"user" => user_params}?

This is a question about Phoenix controllers. The code below references a generated app that was created using the command:

phx.gen.html Accounts User users username:string email:unique password_hash string

In the function bellow the user_params argument returns the map listed in comments.

 def create(conn, user_params ) do
    IO.inspect user_params

    # user_params returns the following:
     
    # %{
    #   "_csrf_token" => "GwUOLhQYeUMnBXsyITsEUQYYAxkVAAAAu7gVbIN7b6BpxbugToahew==",
    #   "_utf8" => "âś“",
    #   "user" => %{
    #   "email" => "adada@aad.com",
    #   "password_hash" => "asdasdasd",
    #   "username" => "adadadasd"
    # }



  end

However, the following code is different and uses a pattern match to get the “user” key data.

  def create(conn, %{"user" => user_params}) do
    IO.inspect user_params
    # pattern matches on "user" and returns the following:

    # "user" => %{
    #   "email" => "adada@aad.com",
    #   "password_hash" => "asdasdasd",
    #   "username" => "adadadasd"
    # }

  end

Can someone explain how this happens?

If I do the following it does not work (nor would I expect it to):

defmodule App do
  def create(params) do
    IO.inspect(params)
  end
end

some_params = %{
  "_csrf_token" => "GwUOLhQYeUMnBXsyITsEUQYYAxkVAAAAu7gVbIN7b6BpxbugToahew==",
  "_utf8" => "âś“",
  "user" => %{
    "email" => "adada@aad.com",
    "password_hash" => "asdasdasd",
    "username" => "adadadasd"
  }
}

App.create(%{"user" => some_params}) # Does not give me the user map

You should…

defmodule App do
  def create(%{"user" => params}) do
    IO.inspect(params)
  end
end

App.create(some_params)

To make it work, the pattern match should be inside the create method.

1 Like

oops. That was a dolt move.on my part.

I still am not sure how this “works”.

To take anothe example, if I were to write it sequentially:

some_params = %{
  "_csrf_token" => "GwUOLhQYeUMnBXsyITsEUQYYAxkVAAAAu7gVbIN7b6BpxbugToahew==",
  "_utf8" => "âś“",
  "user" => %{
    "email" => "adada@aad.com",
    "password_hash" => "asdasdasd",
    "username" => "adadadasd"
  }
}

%{"user" => some_params}

IO.inspect(some_params) // no change

Talking in high level abstraction, your controller function is pattern matched wit the incoming request, there you say to match “a map, containing a user key with some value”. Basically if in your request does not have this user key it will get rejected making the controller throw an error, because no function matched.

In your example the map that corresponds to the incoming request is your “some_params” because inside it have a user key

1 Like

This is just a map. It creates a new map with key “user” and some_params as value.

%{"user" => some_params}

But this is a pattern match

%{"user" => user_params} = some_params

And Elixir is immutable, so this is normal.

IO.inspect(some_params) // no change

And this is rebinding

%{"user" => some_params} = some_params
3 Likes

Extending your example to illustrate @kokolegorille’s answer:

some_params = %{
  "_csrf_token" => "GwUOLhQYeUMnBXsyITsEUQYYAxkVAAAAu7gVbIN7b6BpxbugToahew==",
  "_utf8" => "âś“",
  "user" => %{
    "email" => "adada@aad.com",
    "password_hash" => "asdasdasd",
    "username" => "adadadasd"
  }
}

%{"user" => user_params} = some_params

IO.inspect(some_params) # no change

IO.inspect(user_params) 
# %{
  #"email" => "adada@aad.com",
  #"password_hash" => "asdasdasd",
  #"username" => "adadadasd"
#}
3 Likes

Just to put it into simple sentences here, and lets remove conn for a second.

Given an input like :

%{
  "_csrf_token" => "GwUOLhQYeUMnBXsyITsEUQYYAxkVAAAAu7gVbIN7b6BpxbugToahew==",
  "_utf8" => "âś“",
  "user" => %{
    "email" => "adada@aad.com",
    "password_hash" => "asdasdasd",
    "username" => "adadadasd"
  }
}

# I will accept the input and bind it to `user_params`
def create(user_params) do

# I will accept the input if it has a key named `user`
# and then bind the value of `user` to `user_params`
def create(%{"user" => user_params}) do

# I will accept the input if it has a key named `user`
# and then bind the value of `user` to `user_params`
# and I will bind the whole value to `some_params`
def create(%{"user" => user_params} = some_params) do

So all three functions will accept the input but

  • The first function will just bind the value to user_params
  • The second function will extract the "user" key from the input and give you the value of "user"
    • In this case a map with the email, password_hash and username
  • The third function will do both

You can express outside of a function head like

some_params = %{ "foo" => "bar", "user" => %{ "email" => "foo@bar.com" }}

%{"user" => user_params} = some_params

IO.puts user_params.email # => "foo.bar.com"

You can also go deeper again like :

some_params = %{ "foo" => "bar", "user" => %{ "email" => "foo@bar.com" }}

%{"user" =>%{ "email" => email }} = some_params

IO.puts email # => "foo.bar.com"

Hope that wasn’t too confusing :sweat_smile:

2 Likes

The following example made it clear what is happening.

Explicit pattern match example

defmodule App do
  def create(data) do
    %{"users" => data} = data    # Explicit pattern match
    IO.inspect(data)
  end
end

data = %{
  "users" => "some users and stuff",
  "other_junk" => "blah ...junk"
}

App.create(data)

Less Explicit Example

defmodule App do
  def create(%{"users" => data}) do
    IO.inspect(data)
  end
end

data = %{
  "users" => "some users and stuff",
  "other_junk" => "blah ...junk"
}


App.create(data)    # %{"users" => data} = data

When I was originally looking at the code I thought there was some weird behavior so that when a map is set as a function parameter then any arguments passed into that slot as an argument are not matched on the map but instead are passed into the map…or something :confused:

The underlying syntax is

def create(%{"user" => data} = params) do
  ...
end

create(params)

It is safe to ignore params, because You don’t use it after. Or use _ to explicitly dismiss params.

def create(%{"user" => data} = _params) do
  ...
end

It is better to do function header pattern match, than inside body. You can combine with guard clause.

def create(%{"user" => %{"name" => name}} = _params) when name in ["koko", "admin"] do
  ...
end