Mastering Spectral: Type-Safe Data Conversion in Elixir Spectral is a revolutionary open-source Elixir library that transforms your existing @type specifications and structs into the ultimate single source of truth for validation, serialization, and API generation. Historically, Elixir developers had to rely on separate tools or duplicate logic—defining schemas in Ecto, writing separate validation pipelines, and manually updating JSON schemas or OpenAPI documents. Inspired by Python’s highly successful Pydantic package, Spectral bridges the gap between static typespec documentation and runtime enforcement. It reads type definitions directly out of the compiled BEAM bytecode to achieve seamless, type-safe data conversion with zero boilerplate. 1. Install and Configure Spectral
To run Spectral, ensure your system is updated to at least Erlang/OTP 27+, as the library leverages the highly efficient native json module embedded in the runtime.
Open your mix.exs file and append the dependency to your configuration: def deps do [ {:spectral, “~> 0.13.0”} ] end Use code with caution.
By default, Spectral relies on the debug_info chunk in compiled BEAM files to safely extract your type metadata. Because standard Mix projects enable this flag natively, no further project configuration is required to get started. 2. Define Your Type Specs
Instead of managing validation rule blocks alongside your structs, you simply declare standard Elixir @type properties. Let’s construct an internal representations module representing an e-commerce item and user role permissions.
defmodule MyApp.Store.Product do defstruct [:id, :name, :price, :status, :tags] @type status :: :in_stock | :out_of_stock | :backordered @type t :: %MODULE{ id: binary(), name: String.t(), price: float(), status: status(), tags: [String.t()] } end Use code with caution.
Through this plain struct definition, Spectral reads the map shapes, lists, native types (like strings or floats), and explicit atom enums (status) automatically. 3. Convert JSON with Type Safety
To parse untrusted external inputs into your application boundaries safely, use the Spectral.decode/2 function. This validates that the structural requirements of the target type match the payload exactly.
# Raw incoming JSON data string json_input = “”” { “id”: “prod_9910”, “name”: “Mechanical Keyboard”, “price”: 149.99, “status”: “in_stock”, “tags”: [“hardware”, “ergonomic”] } “”” # Type-safe parsing execution case Spectral.decode(json_input, MyApp.Store.Product) do {:ok, %MyApp.Store.Product{} = product} -> # Guaranteed valid struct with fields correctly transformed IO.inspect(product) {:error, errors} -> # Concrete error mapping with precise field pathways IO.inspect(errors) end Use code with caution.
When decoding, Spectral automatically coerces valid JSON fields into their appropriate Elixir primitives—such as converting the string “in_stock” into the correct native atom :in_stock based entirely on the target typespec. 4. Handle Detailed Validation Failures
If an external client passes invalid or malformed data, Spectral stops execution at the boundary and produces comprehensive, structural error reports containing explicit location information.
Consider this problematic payload where price is accidentally delivered as a text string and an unknown status is given:
malformed_json = “”” { “id”: “prod_0001”, “name”: “Desk Mat”, “price”: “twenty_dollars”, “status”: “discontinued”, “tags”: [] } Use code with caution.
Passing this payload into Spectral.decode(malformed_json, MyApp.Store.Product) outputs targeted error lists indicating exact locations: Error Message Expected Type [“price”] Expected a float value, received string float() [“status”]
Value does not match union members: :in_stock | :out_of_stock | :backordered status()
This precise breakdown eliminates runtime errors deep down within your system architecture, letting your core application logic operate under the absolute guarantee that incoming fields match your assumptions. 5. Generate OpenAPI Specifications
Maintaining manual OpenAPI sync configurations with your code introduces error-prone friction. Spectral elegantly resolves this by exposing automated generation APIs that read your structs and translate them directly into production-ready schemas.
# Automatically generate a compliant OpenAPI 3.1 schema component map {:ok, schema} = Spectral.OpenAPI.component_schema(MyApp.Store.Product) IO.inspect(schema) Use code with caution.
The output gives you a completely mapped Open API definition fragment automatically:
%{ “type” => “object”, “properties” => %{ “id” => %{“type” => “string”}, “name” => %{“type” => “string”}, “price” => %{“type” => “number”}, “status” => %{“type” => “string”, “enum” => [“in_stock”, “out_of_stock”, “backordered”]}, “tags” => %{“type” => “array”, “items” => %{“type” => “string”}} }, “required” => [“id”, “name”, “price”, “status”, “tags”] } Use code with caution.
Integrating this schema into your web pipeline lets you instantly supply interactive documentation platforms like Swagger or Scalar with up-to-date type maps extracted straight from your core codebase. ✅ Mastering Spectral Key Takeaways
Spectral enables Elixir developers to use a single type definition as the absolute source of truth for validation, serialization, and OpenAPI documentation. By shifting your data validation directly to compilation specs, your application boundaries remain secure while eliminating duplicate boilerplates entirely. To tailor this to your current setup, let me know:
What external protocols you are looking to integrate with Spectral (e.g., Phoenix endpoints, message queues)?
If your application uses complex typespec structures like nested custom structs or complex dynamic union definitions? spectral – Hex.pm