Build Status Coverage Status Hex version

TomlElixir is a modern, high-performance TOML parser and encoder for Elixir. It is designed to be fully compliant with the TOML specification while providing an idiomatic Elixir experience.

Features

  • Full Specification Support: Supports both TOML 1.0.0 and 1.1.0 specifications.
  • Native Elixir Types: Decodes TOML directly into native Elixir types (DateTime, NaiveDateTime, Date, Time, Float, Integer, Boolean, String, Map, List).
  • Bidirectional: Full support for both decoding (TOML to Map) and encoding (Map to TOML).
  • Strict Parsing: Comprehensive error messages for invalid TOML documents.
  • Spec Selection: Allows you to choose which TOML version to follow during decoding.
  • Struct Support: Use the TomlElixir.Encoder protocol to encode custom structs via @derive.
  • Format-Preserving Editing: TomlElixir.Document changes values in a TOML file while keeping its comments, whitespace, and key order.

Installation

Add toml_elixir to your list of dependencies in mix.exs:

def deps do
  [
    {:toml_elixir, "~> 3.3"}
  ]
end

Usage

Decoding TOML

Convert a TOML string into an Elixir Map.

toml_string = """
title = "TOML Example"

[database]
enabled = true
ports = [ 8000, 8001, 8002 ]
temp_targets = { cpu = 79.5, case = 72.0 }
"""

# Simple decoding
{:ok, data} = TomlElixir.decode(toml_string)
data = TomlElixir.decode!(toml_string)

# Specify TOML version (default is :"1.1.0")
TomlElixir.decode(toml_string, spec: :"1.0.0")

Keys are strings by default. Use keys: :atoms! to convert them to atoms that already exist; a key without one fails decoding, so documents cannot create new atoms. For tighter control, pass a function such as an explicit mapping:

TomlElixir.decode("[server]\nport = 80", keys: :atoms!)
#=> {:ok, %{server: %{port: 80}}}

mapping = %{"server" => :server, "port" => :port}
TomlElixir.decode("[server]\nport = 80", keys: &Map.fetch!(mapping, &1))
#=> {:ok, %{server: %{port: 80}}}

Use :transform to convert or validate values as they are decoded. The callback gets the key path and the decoded value, children before their parent table, and returns the value to use. Raise to reject a value:

transform = fn
  ["server", "port"], port when port not in 1..65_535 -> raise ArgumentError, "port out of range"
  ["server"], %{"host" => host, "port" => port} -> %MyApp.Server{host: host, port: port}
  _path, value -> value
end

TomlElixir.decode("[server]\nhost = \"localhost\"\nport = 8080", transform: transform)
#=> {:ok, %{"server" => %MyApp.Server{host: "localhost", port: 8080}}}

Encoding to TOML

Convert an Elixir Map back into a valid TOML string.

data = %{
  "title" => "TOML Example",
  "database" => %{
    "enabled" => true,
    "ports" => [8000, 8001, 8002]
  }
}

{:ok, toml_string} = TomlElixir.encode(data)
toml_string = TomlElixir.encode!(data)

# Optional layout controls; the default output is unchanged
TomlElixir.encode!(data, multiline_arrays: true, inline_tables: [["database"]])

# Iodata for APIs that accept it, such as File.write/3
{:ok, iodata} = TomlElixir.encode_to_iodata(data)
File.write!("config.toml", TomlElixir.encode_to_iodata!(data))

Editing TOML Without Losing Formatting

TomlElixir.Document edits a document in place. Untouched parts render back exactly as written, including comments, key order, and value formats such as hex integers. New keys and tables are placed next to related content. See the TomlElixir.Document docs for the placement rules and current limits.

toml = """
# Server settings
[server]
port = 0x1F90 # hex on purpose
"""

doc =
  toml
  |> TomlElixir.Document.parse!()
  |> TomlElixir.Document.put!(["server", "port"], 9090)
  |> TomlElixir.Document.put!(["server", "host"], "0.0.0.0")

TomlElixir.Document.to_string(doc)
#=> "# Server settings\n[server]\nport = 9090 # hex on purpose\nhost = \"0.0.0.0\"\n"

Errors

Decoding errors include where the parser detected the problem. line and column are 1-based, with columns counted in Unicode codepoints; offset is a 0-based byte offset into the input.

{:error, %TomlElixir.Parser.Error{reason: "Invalid value nope", line: 2, column: 5, offset: 10} = error} =
  TomlElixir.decode("a = 1\nb = nope")

Exception.message(error)
#=> "Invalid value nope at line 2, column 5"

Encoding errors include the path to the value that could not be encoded:

{:error, %TomlElixir.Encoder.Error{path: ["servers", 1, "port"]} = error} =
  TomlElixir.encode(%{servers: [%{port: 80}, %{port: nil}]})

Exception.message(error)
#=> "servers[1].port: nil is not supported in TOML"

Encoding Structs

You can use the TomlElixir.Encoder protocol to allow your structs to be encoded as TOML.

defmodule User do
  @derive TomlElixir.Encoder
  defstruct [:name, :age]
end

user = %User{name: "Alice", age: 30}
toml_string = TomlElixir.encode!(%{user: user})
# [user]
# age = 30
# name = "Alice"

You can also choose which struct keys are encoded:

defmodule User do
  @derive {TomlElixir.Encoder, only: [:name]}
  defstruct [:name, :age, :password]
end

defmodule PublicUser do
  @derive {TomlElixir.Encoder, except: [:password]}
  defstruct [:name, :age, :password]
end

Prefer :only to avoid leaking new fields added to a struct in the future.

If you do not own the struct module, you can derive the protocol externally:

Protocol.derive(TomlElixir.Encoder, NameOfTheStruct, only: [:public_field])
Protocol.derive(TomlElixir.Encoder, NameOfTheStruct, except: [:private_field])
Protocol.derive(TomlElixir.Encoder, NameOfTheStruct)

Type Mapping

TOML TypeElixir Type
StringString.t()
IntegerInteger.t()
FloatFloat.t() (plus :infinity, :neg_infinity, :nan)
Booleanboolean()
Offset Date-TimeDateTime.t()
Local Date-TimeNaiveDateTime.t()
Local DateDate.t()
Local TimeTime.t()
ArrayList.t()
TableMap.t()
Inline TableMap.t()

Contribution

Contributions are welcome! If you find a bug or want to suggest a feature, please open an issue or submit a pull request. Make sure all tests pass before submitting.

# Run tests
mix test

# Run coverage report
mix coveralls

# Run decode, encode, and document editing benchmarks separately (time, memory, and reductions)
mix run bench/decode.exs
mix run bench/encode.exs
mix run bench/document.exs

License

TomlElixir is released under the MIT License.