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.
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?
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
lng fields are
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
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.
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.