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.Encoderprotocol to encode custom structs via@derive. - Format-Preserving Editing:
TomlElixir.Documentchanges 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"}
]
endUsage
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]
endPrefer :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 Type | Elixir Type |
|---|---|
| String | String.t() |
| Integer | Integer.t() |
| Float | Float.t() (plus :infinity, :neg_infinity, :nan) |
| Boolean | boolean() |
| Offset Date-Time | DateTime.t() |
| Local Date-Time | NaiveDateTime.t() |
| Local Date | Date.t() |
| Local Time | Time.t() |
| Array | List.t() |
| Table | Map.t() |
| Inline Table | Map.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.