README.md

<p align="center">
  <img src="https://raw.githubusercontent.com/pauldemarco/auto_struct/main/assets/logo.png" alt="AutoStruct logo" width="420">
</p>

# AutoStruct

Generated structs for Elixir from JSON Schema.

AutoStruct is a thin code generation layer over [Exonerate](https://hexdocs.pm/exonerate/).
Exonerate validates JSON-shaped Elixir terms generated from [JSON Schema](https://json-schema.org/);
AutoStruct generates structs, conversion helpers, and encoder implementations around that validator.

## Installation

The package can be installed by adding `auto_struct` to your list of
dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:auto_struct, "~> 0.3.0"}
  ]
end
```

Documentation is available on [HexDocs](https://hexdocs.pm/auto_struct).

## Usage

```elixir
defmodule Person do
  use AutoStruct.JsonSchema,
    schema: """
    {
      "$id": "https://example.com/person.schema.json",
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "title": "Person",
      "type": "object",
      "properties": {
        "first_name": {
          "type": "string",
          "description": "The person's first name."
        },
        "age": {
          "description": "Age in years which must be equal to or greater than zero.",
          "type": "integer",
          "minimum": 0
        },
        "happy": {
          "type": "boolean",
          "default": true,
          "description": "Whether the person is happy."
        },
        "attrs": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "Optional array of attributes.",
          "nullable": true
        }
      },
      "required": ["first_name"]
    }
    """
end

# `new/1` returns {:ok, value} | {:error, reason}
iex> Person.new(first_name: "George", age: 31)
{:ok, %Person{first_name: "George", age: 31, happy: true, attrs: nil}}

# `new!/1` raises on error
iex> Person.new!(first_name: "George", age: 31, attrs: %{phone: 123})
** Some Error Message from the underlying validation library

# `from_json/1` returns {:ok, value} | {:error, reason}
iex> Person.from_json(%{"first_name" => "George", "age" => 31})
{:ok, %Person{first_name: "George", age: 31, happy: true, attrs: nil}}

iex> Person.from_json(%{"first_name" => "George", "age" => 31, "attrs" => %{"phone" => 123}})
** Some Error Message from the underlying validation library

# Generated structs implement Elixir's built-in JSON.Encoder.
iex> JSON.encode!(Person.new!(first_name: "George"))
"{\"age\":null,\"attrs\":null,\"first_name\":\"George\",\"happy\":true}"

# If you would like to build the struct directly, and avoid the internal cast to json map
# and back again, you can skip validation but still enforce the keys:
iex> %Person{first_name: "Hello"}

```

## Examples

Runnable examples are available in the `examples/` directory:

```bash
mix run examples/inline_schema.exs
mix run examples/file_schema.exs
```

## Mix Tasks

### mix auto_struct.gen.modules

Generate the Module stubs from a folder of schema files.

```
mix auto_struct.gen.modules <path_to_schemas> <output_path_modules>
```

## How it works

Under the hood the `use AutoStruct.JsonSchema` macro uses Exonerate to generate the validation function needed
by `new/1`, `from_json/1`, and `validate/1`.

AutoStruct creates a `defstruct` with an atom key for every top-level property in the JSON Schema.
Property names are preserved as-is. For properties marked as `required`, AutoStruct also includes
them in `@enforce_keys`.

`new/1` and `validate/1` recursively normalize structs, maps, and lists into JSON-shaped terms before
calling Exonerate. Nested maps and arrays are validated by Exonerate, but `from_json/1` only casts the
top-level object into a struct; nested objects remain JSON-shaped maps unless you transform them yourself.

Generated modules expose schema metadata:

```elixir
Person.__schema__(:json)
Person.__schema__(:fields)
Person.__schema__(:required)
```

Elixir's built-in `JSON.Encoder` is the primary encoder. When Jason is available, AutoStruct also emits
a compatible `Jason.Encoder` implementation.

## Limitations

Nested JSON Schema objects and arrays are validated by Exonerate, but AutoStruct only casts the top-level
schema object into a struct. Nested objects returned from `from_json/1` remain string-keyed maps unless you
transform them yourself.

AutoStruct currently generates fields from top-level schema properties. Advanced JSON Schema composition
features are delegated to Exonerate for validation and are not expanded into additional struct fields.