# get_in/2 needs Access.key!/1 for structs

`get_in/2`, `put_in/3`, and `update_in/3` walk a path of atom keys through maps and keyword lists just fine — but the same atom path raises on a struct.

```elixir
defmodule Profile do
  @type t :: %__MODULE__{}

  defstruct [:city, :country]
end

defmodule User do
  @type t :: %__MODULE__{}

  defstruct [:name, :profile]
end

user = %User{name: "Ada", profile: %Profile{city: "London", country: "UK"}}

get_in(user, [:profile, :city])
# ** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour
#
# You can use the "struct.field" syntax to access struct fields. You can also use Access.key!/1 to access struct fields dynamically inside get_in/put_in/update_in). Make sure the module name is correct and has been specified in full (or that an alias has been defined)

get_in(user, [Access.key!(:profile), Access.key!(:city)])
# => "London"
```

`get_in/2` treats a plain atom in the path as a call to `Access.fetch/2` on whatever module owns the current value. `Map` implements the `Access` behaviour, so atom keys work against plain maps and keyword lists. Structs deliberately do not implement `Access` — Elixir's authors want a typo'd struct field to fail loudly (a `KeyError` or, here, an `UndefinedFunctionError`) instead of silently returning `nil` the way a missing map key would. `Access.key!/1` and `Access.key/2` are the accessor functions that opt a struct field back into the `get_in`/`put_in`/`update_in` machinery, explicitly, one key at a time.

The two accessor functions differ on a missing key:

```elixir
Access.key!(:profile)
# raises KeyError if :profile is not a key on the struct

Access.key(:profile, %Profile{})
# returns the default (here, a fresh %Profile{}) instead of raising
```

`Access.key!/1` on a key the struct genuinely doesn't have:

```elixir
get_in(user, [Access.key!(:profile), Access.key!(:missing_field)])
# ** (KeyError) key :missing_field not found in:
#
#     %Profile{city: "London", country: "UK"}
```

The same `Access.key!/1` path works for writes too:

```elixir
put_in(user, [Access.key!(:profile), Access.key!(:city)], "Paris")
# => %User{name: "Ada", profile: %Profile{city: "Paris", country: "UK"}}

update_in(user, [Access.key!(:profile), Access.key!(:city)], &String.upcase/1)
# => %User{name: "Ada", profile: %Profile{city: "LONDON", country: "UK"}}
```

Two more gotchas. First, a `nil` intermediate value blows up the same way a missing struct key does — `get_in` doesn't short-circuit through `nil` for you, so a chain like `Access.key!(:profile)` on a `User` whose `profile` is `nil` fails at the next step, not silently. Second, since Elixir 1.17, if you already know every step of the path exists, plain dot access works directly on structs without any of this: `user.profile.city` returns `"London"` with no `Access` involved at all — reach for `get_in`/`Access.key!/1` specifically when the path is built dynamically or needs to survive an absent key gracefully.

[Access.key!/1 docs](https://hexdocs.pm/elixir/Access.html#key!/1)


---

Created by: almirsarajcic
Date: August 30, 2026
URL: https://elixirdrops.net/d/TgPNG7nA
