{"id":29925485,"url":"https://github.com/expressapp/construct","last_synced_at":"2025-12-11T23:54:18.484Z","repository":{"id":27839255,"uuid":"115333696","full_name":"ExpressApp/construct","owner":"ExpressApp","description":"Library for dealing with data structures","archived":false,"fork":false,"pushed_at":"2025-02-01T10:14:32.000Z","size":202,"stargazers_count":52,"open_issues_count":0,"forks_count":8,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-07-27T16:59:29.056Z","etag":null,"topics":["data","elixir","elixir-construct","elixir-lang","types","validation"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ExpressApp.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2017-12-25T11:44:31.000Z","updated_at":"2025-02-01T10:14:35.000Z","dependencies_parsed_at":"2023-07-15T12:02:50.161Z","dependency_job_id":null,"html_url":"https://github.com/ExpressApp/construct","commit_stats":{"total_commits":127,"total_committers":3,"mean_commits":"42.333333333333336","dds":0.03149606299212604,"last_synced_commit":"38acc7c1eb0c6cdaa11568f7e2393dd93eb8841a"},"previous_names":["expressapp/struct"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/ExpressApp/construct","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExpressApp%2Fconstruct","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExpressApp%2Fconstruct/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExpressApp%2Fconstruct/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExpressApp%2Fconstruct/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ExpressApp","download_url":"https://codeload.github.com/ExpressApp/construct/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExpressApp%2Fconstruct/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268380162,"owners_count":24241203,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-02T02:00:12.353Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["data","elixir","elixir-construct","elixir-lang","types","validation"],"created_at":"2025-08-02T11:37:13.658Z","updated_at":"2025-12-11T23:54:18.452Z","avatar_url":"https://github.com/ExpressApp.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Construct [![Hex.pm](https://img.shields.io/hexpm/v/construct.svg)](https://hex.pm/packages/construct)\n\n---\n\nLibrary for dealing with data structures\n\n---\n\n* [Installation](#installation)\n* [Usage](#usage)\n* [Types](#types)\n* [Construct definition](#construct-definition)\n* [Errors while making structures](#errors-while-making-structures)\n\n---\n\n## Installation\n\n1. Add `construct` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [{:construct, \"~\u003e 2.0\"}]\nend\n```\n\n2. Ensure `construct` is started before your application:\n\n```elixir\ndef application do\n  [applications: [:construct]]\nend\n```\n\n## Usage\n\nSuppose you have some user input from several sources (DB, HTTP request, WebSocket), and you will need to process that data into something type-validated, like User entity. With this library you can define a type-validated structure for this entity:\n\n```elixir\ndefmodule User do\n  use Construct do\n    field :name\n    field :age, :integer\n  end\nend\n```\n\nAnd use it to cast your data into something identical, to prevent type coercion in different places of your code. Like this:\n\n```elixir\niex\u003e User.make(%{\"name\" =\u003e \"John Doe\", \"age\" =\u003e \"37\"})\n{:ok, %User{age: 37, name: \"John Doe\"}}\n```\n\nPretty neat, yeah? But what if you need more complex type? We have a solution!\n\n```elixir\ndefmodule Answer do\n  @behaviour Construct.Type\n\n  def cast(\"yes\"), do: {:ok, true}\n  def cast(\"no\"), do: {:ok, false}\n  def cast(_), do: {:error, :invalid_answer}\nend\n```\n\nAnd use it in your structure like this:\n\n```elixir\ndefmodule Quiz do\n  use Construct do\n    field :user_id, :integer\n    field :answers, {:array, Answer}\n  end\nend\n```\n\n```elixir\niex\u003e Quiz.make(%{user_id: 42, answers: [\"yes\", \"no\", \"no\", \"yes\"]})\n{:ok, %Quiz{answers: [true, false, false, true], user_id: 42}}\n```\n\n\u003e What if we need to parse 'optimized' query string from URL, like list of user ids separated by a comma? Do we need to create a custom type for each boxed type?\n\nNo! Just use type composition feature:\n\n```elixir\ndefmodule CommaList do\n  @behaviour Construct.Type\n\n  def cast(\"\"), do: {:ok, []}\n  def cast(v) when is_binary(v), do: {:ok, String.split(v, \",\")}\n  def cast(v) when is_list(v), do: {:ok, v}\n  def cast(_), do: :error\nend\n\ndefmodule SearchFilterRequest do\n  use Construct do\n    field :user_ids, [CommaList, {:array, :integer}], default: []\n  end\nend\n```\n\n(Use `CommaList` type from [construct_types](https://github.com/ExpressApp/construct_types/blob/master/lib/types/comma_list.ex) package).\n\n```elixir\niex\u003e SearchFilterRequest.make(%{\"user_ids\" =\u003e \"1,2,42\"})\n{:ok, %SearchFilterRequest{user_ids: [1, 2, 42]}}\n```\n\nAlso we have `default` option in our `user_ids` field:\n\n```elixir\niex\u003e SearchFilterRequest.make(%{})\n{:ok, %SearchFilterRequest{user_ids: []}}\n```\n\n\u003e What if I have a lot of identical code?\n\nYou can use already defined structures as types:\n\n```elixir\ndefmodule Comment do\n  use Construct do\n    field :text\n  end\nend\n\ndefmodule Post do\n  use Construct do\n    field :title\n    field :comments, {:array, Comment}\n  end\nend\n\niex\u003e Post.make(%{title: \"Some article\", comments: [%{\"text\" =\u003e \"cool!\"}, %{text: \"awesome!!!\"}]})\n{:ok, %Post{comments: [%Comment{text: \"cool!\"}, %Comment{text: \"awesome!!!\"}], title: \"Some article\"}}\n```\n\nAnd include repeated fields in structures:\n\n```elixir\ndefmodule PK do\n  use Construct do\n    field :primary_key, :integer\n  end\nend\n\ndefmodule Timestamps do\n  use Construct do\n    field :created_at, :utc_datetime, default: \u0026DateTime.utc_now/0\n    field :updated_at, :utc_datetime, default: nil\n  end\nend\n\ndefmodule User do\n  use Construct do\n    include PK\n    include Timestamps\n\n    field :name\n  end\nend\n\niex\u003e User.make(%{name: \"John Doe\", primary_key: 42})\n{:ok,\n %User{created_at: #DateTime\u003c2018-10-14 20:43:06.595119Z\u003e, name: \"John Doe\",\n  primary_key: 42, updated_at: nil}}\n\niex\u003e User.make(%{name: \"John Doe\", created_at: \"2015-01-23 23:50:07\", primary_key: 42})\n{:ok,\n %User{created_at: #DateTime\u003c2015-01-23 23:50:07Z\u003e, name: \"John Doe\",\n  primary_key: 42, updated_at: nil}}\n```\n\n\u003e What if I don't want to define module to make a nested field?\n\n`field` macro can `do` it for you:\n\n```elixir\ndefmodule User do\n  use Construct do\n    field :name do\n      field :first\n      field :last, :string, default: nil\n    end\n  end\nend\n\niex\u003e User.make(name: %{first: \"John\"})\n{:ok, %User{name: %User.Name{first: \"John\", last: nil}}}\n```\n\nConstruct tries to fit in Elixir as much as it possible:\n\n```elixir\ndefmodule ComplexDefaults do\n  use Construct do\n    field :required\n\n    field :nested do\n      field :key, :string, default: \"nesting 1\"\n\n      field :nested do\n        field :key, :string, default: \"nesting 2\"\n      end\n    end\n  end\nend\n\niex\u003e %ComplexDefaults{}\n** (ArgumentError) the following keys must also be given when building struct ComplexDefaults: [:required]\n    expanding struct: ComplexDefaults.__struct__/1\n\niex\u003e %ComplexDefaults{required: 1}\n%ComplexDefaults{\n  nested: %ComplexDefaults.Nested{\n    key: \"nesting 1\",\n    nested: %ComplexDefaults.Nested.Nested{key: \"nesting 2\"}\n  },\n  required: 1\n}\n```\n\n\u003e What if I want to use union types?\n\nUse custom types:\n\n```elixir\ndefmodule User do\n  use Construct do\n    field :id, :integer\n    field :name\n    field :age, :integer\n  end\nend\n\ndefmodule Bot do\n  use Construct do\n    field :id, :integer\n    field :name\n    field :version\n  end\nend\n\ndefmodule Author do\n  @behaviour Construct.Type\n\n  # here's the trick, just choose the type by yourself, based on keys or value in specific field.\n  # but be careful, because there can be atoms and strings in keys!\n  def cast(%{\"age\" =\u003e _} = v), do: User.make(v)\n  def cast(%{\"version\" =\u003e _} = v), do: Bot.make(v)\n  def cast(_), do: :error\nend\n\ndefmodule Post do\n  use Construct do\n    field :author, Author\n  end\nend\n\niex\u003e Post.make(%{\"author\" =\u003e %{}})\n{:error, %{author: :invalid}}\n\niex\u003e Post.make(%{\"author\" =\u003e %{\"age\" =\u003e \"420\"}})\n{:error, %{author: %{id: :missing, name: :missing}}}\n\niex\u003e Post.make(%{\"author\" =\u003e %{\"id\" =\u003e \"42\", \"name\" =\u003e \"john doe\", \"age\" =\u003e \"420\"}})\n{:ok, %Post{author: %User{age: 420, id: 42, name: \"john doe\"}}}\n\niex\u003e Post.make(%{\"author\" =\u003e %{\"id\" =\u003e \"42\", \"name\" =\u003e \"john doe\", \"version\" =\u003e \"1.0.0\"}})\n{:ok, %Post{author: %Bot{id: 42, name: \"john doe\", version: \"1.0.0\"}}}\n```\n\n\u003e How can I serialize my structures with Jason?\n\nUse `@derive` attribute and `derive` option for nested fields:\n\n```elixir\ndefmodule Server do\n  @derive {Jason.Encoder, only: [:name, :operating_system]}\n\n  use Construct do\n    field :name\n    field :password\n\n    field :operating_system, derive: Jason.Encoder do\n      field :name, :string\n      field :arch, :string, default: \"x86\"\n    end\n  end\nend\n\niex\u003e {:ok, server} = Server.make(name: \"example\", password: \"secret\", operating_system: %{name: \"MacOS\"})\n{:ok,\n %Server{\n   name: \"example\",\n   operating_system: %Server.OperatingSystem{arch: \"x86\", name: \"MacOS\"},\n   password: \"secret\"\n }}\n\niex\u003e Jason.encode!(server)\n\"{\\\"name\\\":\\\"example\\\",\\\"operating_system\\\":{\\\"arch\\\":\\\"x86\\\",\\\"name\\\":\\\"MacOS\\\"}}\"\n```\n\n## Types\n\n### Primitive types\n\n* `t()`:\n  * integer\n  * float\n  * boolean\n  * string\n  * binary\n  * decimal\n  * utc_datetime\n  * naive_datetime\n  * date\n  * time\n  * any\n  * array\n  * map\n  * struct\n* `{:array, t()}`\n* `{:map, t()}`\n* `[t()]`\n\n### Complex (custom) types\n\nYou can use Ecto custom types like Ecto.UUID or implement by yourself:\n\n```elixir\ndefmodule CustomType do\n  @behaviour Construct.Type\n\n  @spec cast(term) :: {:ok, term} | {:error, term} | :error\n  def cast(value) do\n    {:ok, value}\n  end\nend\n```\n\nNotice that `cast/1` can return error with reason, this behaviour is supported only by Struct and you can't use types defined using Construct in Ecto schemas.\n\n## Construct definition\n\n```elixir\ndefmodule User do\n  use Construct, struct_opts\n\n  structure do\n    include module_name\n\n    field name\n    field name, type\n    field name, type, field_opts\n  end\nend\n```\n\nWhere:\n\n* `use Construct, struct_opts` where:\n  * `struct_opts` — options passed to every `make/2` and `make!/2` calls as default options;\n* `include module_name` where:\n  * `module_name` — is struct module, that validates for existence in compile time;\n* `field name, type, field_opts` where:\n  * `name` — atom;\n  * `type` — primitive or custom type, that validates for existence in compile time;\n  * `field_opts`.\n\n## Errors while making structures\n\nWhen you provide invalid data to your structures you can get tuple with errors as maps:\n\n```elixir\niex\u003e Post.make\n{:error, %{comments: :missing, title: :missing}}\n\niex\u003e Post.make(%{comments: %{}, title: :test})\n{:error, %{comments: :invalid, title: :invalid}}\n\niex\u003e Post.make(%{comments: [%{}], title: \"what the title?\"})\n{:error, %{comments: %{text: :missing}}}\n```\n\nOr receive an exception with invalid data:\n\n```elixir\niex\u003e Post.make!\n** (Construct.MakeError) %{comments: {:missing, nil}, title: {:missing, nil}}\n    iex:10: Post.make!/2\n\niex\u003e Post.make!(%{comments: %{}, title: :test})\n** (Construct.MakeError) %{comments: {:invalid, %{}}, title: {:invalid, :test}}\n    iex:10: Post.make!/2\n\niex\u003e Post.make!(%{comments: [%{}], title: \"what the title?\"})\n** (Construct.MakeError) %{comments: %{text: {:missing, [nil]}}}\n    iex:10: Post.make!/2\n```\n\n---\n\n### Contributing\n\n1. Fork it\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexpressapp%2Fconstruct","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexpressapp%2Fconstruct","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexpressapp%2Fconstruct/lists"}