Today I Learned

Why you shouldn’t use map fields in schemas with Absinthe

| 3 min read

When we structure our schemas, there is sometimes a need to store a nested object inside. In most cases these nested fields are well-defined, but because we are lazy, we often use a field with :map type. Sometimes it can lead to some unforeseen problems. Be especially careful when using such schemas with GraphQL and Absinthe.

Let’s suppose we have to store parking spots information in the database. Here’s a simple schema, which holds its status as string and a location inside a map.

schema "parking_spots" do
   field :status, :string
   field :location, :map
 end

In Absinthe we have a query for parking spot retrieval and mutation for adding a new parking spot.

object :parking_spot do
   field :status, non_null(:string)
   field :location, non_null(:location)
 end
 
 object :location do
   field :lat, :float
   field :lng, :float
 end
 
 query do
   field :get_parking_spot, :parking_spot do
     arg :id, non_null(:id)
 
     resolve &Resolvers.get_parking_spot/3
   end
 end
 
 input_object :location_params do
   field :lat, non_null(:float)
   field :lng, non_null(:float)
 end
 
 mutation do
   field :add_parking_spot, :parking_spot do
     arg :status, non_null(:string)
     arg :location, non_null(:location_params)
    
     resolve &Resolvers.add_parking_spot/3
   end
 end

Let’s try to use our queries in GraphQL’s playground tool.

1.png

When we add a parking spot, the mutation works and we receive all the information that we provided in the params along with the ID of the new record. The same thing should happen when we try to retrieve this parking in the getParkingSpot query, right?

2.png

As you can see, we took the ID of previously created record and pulled information about parking spot. But we somehow lost the parking spot’s location in the process. Both lat and lng fields are null. It happens because there is no atom type in Ecto, so it converts atom keys in location to strings. Absinthe’s default resolver works on atom maps, so it’s unable to read maps with string keys and returns null for fields inside location.

One of the ways to resolve this problem is to use embedded schema instead of a map for location. With embeds_one Ecto will cast the data into a struct. Because of Ecto serialization, structs will retain atom keys and maps will have its keys converted to strings.

defmodule ParkingMatch.Parking.Location do
 use Ecto.Schema
 
 embedded_schema do
   field :lat, :float
   field :lng, :float
 end
end
 
defmodule ParkingMatch.Parking do
 use ParkingMatch.Schema
 
 schema "parking_spots" do
   field :status, :string
   embeds_one :location, ParkingMatch.Parking.Location
 end
...

When we go back and try to run our query from Playground again, we now receive a proper response.

3.png

Thanks to one additional module with embedded schema inside, we have gained a great deal. Absinthe will now properly resolve our location field, since it receives an atom map. We don’t have to clutter our schema with additional custom resolvers. The other advantage of this solution is that we now have a well-defined structure, which verifies the data inside location and can leverage changeset for further validation.