Spark-based DSL for declaring validated, sanitized, immutable structs with rich introspection. Optional Ash 3.x integration through the GuardedStruct.AshResource extension.

defmodule MyApp.User do
  use GuardedStruct

  guardedstruct do
    field :email, :string,
      enforce: true,
      derives: "sanitize(trim, downcase) validate(string, not_empty, email_r)"

    field :nickname, :string, derives: "sanitize(trim) validate(string, max_len=24)"

    sub_field :profile, :map do
      field :bio, :string, derives: "validate(string, max_len=200)"
    end
  end
end

MyApp.User.builder(%{email: "  Alice@X.IO  "})
# => {:ok, %MyApp.User{email: "alice@x.io", ...}}

Map

TopicSub-rule
field / sub_field / conditional_field / virtual_field / dynamic_field and section optionsguarded_struct:dsl
derives: string mini-language; built-in sanitize/validate opsguarded_struct:derive
conditional_field runtime dispatch and error aggregationguarded_struct:conditional
Per-field validator: and section-level main_validator:guarded_struct:validators
Cross-field auto:, from:, on:, domain:guarded_struct:core-keys
Custom ops via use GuardedStruct.Derive.Extensionguarded_struct:extensions
GuardedStruct.AshResource — same DSL inside use Ash.Resourceguarded_struct:ash
Builder, Validate, Diff, Info runtime APIguarded_struct:api
Error shape, Splode wrapping, telemetryguarded_struct:errors

Runnable walkthrough

The guidance/guarded-struct.livemd LiveBook runs every public feature end-to-end against a fresh BEAM. Open it in Livebook and Run all to verify the contract claims below match the installed version.

Universal contracts

  • Module.builder/1,2 returns {:ok, %Module{}} or {:error, [error_map]}. The error tuple's second element is always a list (never a single map).
  • Every error map has the canonical shape: %{field: atom(), action: atom(), message: String.t(), [errors: [error_map]]}. Multi-field errors (:required_fields, :authorized_fields) emit one entry per field.
  • Sanitizer / validator pipelines use pipe-friendly order: value |> sanitize(:op).
  • All section + field metadata is parsed at compile time. __information__/0, __fields__/0, __field_meta__/1, __guarded_field_name_set__/0 (Ash) are baked into the generated module — no runtime introspection on the hot path.

Compile-time guarantees

The Spark layer runs verifiers that reject the module at compile time when:

  • a validator: {Mod, :fn} MFA doesn't export the function (VerifyValidatorMFA),
  • an auto: {Mod, :fn} MFA doesn't exist (VerifyAutoMFA),
  • a struct: or structs: target creates a cycle (VerifyNoStructCycles).

Malformed derives: strings fail compilation with Spark.Error.DslError pointing at the offending field's source line.