Nestru: serialize maps to nested structs

The Nestru library can convert a map of any shape into a model of nested structs according to given hints. And It can convert any nested struct into a map.

The library’s primary purpose is to serialize between JSON map and an application’s model that is a struct having other structs nested in it.

It comes from the idea to have no DSL and less custom code to parse JSONs as an alternative to full-fledged Ecto with schemaless changesets.

Works good in Jason <-> Nestru pair.

There are several possible data validation options:

  • with ex_json_schema applied to the map before passing to Nestru
  • within Nestru gather_fields_map/3 and from_map_hint/3 callbacks
  • with Ecto schemaless changesets applied to the struct
  • with Domo applied to the struct to validate the struct conformance to its @type t() and associated preconditions

See typical usage in Readme.md via:

Run in Livebook

Shortly it looks like:

defmodule Order do
  @derive [
    Nestru.Encoder,
    {Nestru.Decoder, %{items: [LineItem], total: Total}}
  ]
  # Gave a hint to Nestru on how to process the items and total fields
  # others go to a struct as is.

  defstruct [:id, :items, :total]
end

defmodule LineItem do
  @derive [Nestru.Encoder, Nestru.Decoder]
  defstruct [:amount]
end

defmodule Total do
  @derive [Nestru.Encoder, Nestru.Decoder]
  defstruct [:sum]
end

map = %{
  "id" => "A548",
  "items" => [%{"amount" => 150}, %{"amount" => 350}],
  "total" => %{"sum" => 500}
}

{:ok, model} = Nestru.decode_from_map(map, Order)

returns:

{:ok,
  %OrderA{
    id: "A548",
    items: [%LineItemA{amount: 150}, %LineItemA{amount: 350}],
    total: %Total{sum: 500}
  }}

And going back to the map is as simple as that:

map = Nestru.encode_to_map(model)

returns:

%{
  id: "A548",
  items: [%{amount: 150}, %{amount: 350}],
  total: %{sum: 500}
}

More information here:

11 Likes

0.1.1 - November 13, 2021

  • Add has_key?/2 and get/3 map functions that lookup keys in maps both in a binary or an atom form.

  • Add the link to open Readme.md in LiveBook :slightly_smiling_face:

1 Like

I needed exactly this two weeks ago, thanks for making it. :heart:

1 Like

This is 100% exactly the library I needed. Thank you! (and again the extra time you spent on the documentation).

1 Like

0.2.0 - August 16, 2022

  • Fix to ensure the module is loaded before checking if it’s a struct
  • Add decode and encode verbs to function names
  • Support [Module] hint in the map returned from from_map_hint to decode the list of structs
  • Support %{one_key: :other_key} mapping configuration for the PreDecoder protocol in @derive attribute.

Now, for most common decoding cases, it’s possible to configure decoding fully in a declarative way by providing param maps when deriving protocols like that:

defmodule Order do
  @derive [
    {Nestru.PreDecoder, %{"attrs" => :total}},
    {Nestru.Decoder, %{total: Total}}
  ]

  defstruct [:id, :total]
end

defmodule Total do
  @derive Nestru.Decoder
  
  defstruct [:sum]
end

so the decoding works like that:

Nestru.decode_from_map(%{"attrs" => %{"sum" => 568}, "id" => "A874"}, Order)

{:ok, %Order{id: "A874", total: %Total{sum: 568}}}
4 Likes

0.2.1 - August 20, 2022

  • Fix decode_from_map(!)/2/3 to return the error for not a map value
2 Likes

0.3.0 - November 14, 2022

  • Rename Nestru.PreDecoder.gather_fields_map/3 to gather_fields_from_map/3
  • Rename Nestru.Encoder.encode_to_map/1 to Nestru.Encoder.gather_fields_from_struct/2
  • Make encode_to_map(!)/2 work only with structs and add encode_to_list_of_maps(!)/2 for lists
  • Add context parameter to encode_to_* functions

Contexts can be helpful to specify Decoder or Encoder strategy for various senders or receivers of the map.

That is the Nestru.decode_from_map(map, Item, :mobile) call passes :mobile context to def from_map_hint(value, context, map) and the Nestru.encode_to_map(struct, :mobile) call passes the :mobile context to def gather_fields_from_struct(%Item{} = struct, context) implementation for the Item struct appropriately.

So the application’s internal model can be translated into several inbound and outbound models depending on the context :slightly_smiling_face:

1 Like

0.3.1 - November 22, 2022

  • Add :only and :except options for deriving of Nestru.Encoder protocol (hommage for Jason.Encoder approach :slightly_smiling_face:)
  • Add explicit :translate option for deriving of Nestru.PreDecoder protocol
  • Add explicit :hint option for deriving of Nestru.Decoder protocol

See the documentation for examples.

1 Like