github.com/jbox-web/ucl.cr
master / published Jul 26, 2026 / repository
LibUCL bindings for Crystal, easy ;)
LibUCL bindings for Crystal
LibUCL is a universal configuration language.
This shard is a Crystal wrapper around LibUCL. It lets you:
- load UCL/JSON configuration into native Crystal values,
- dump Crystal objects back to UCL, JSON, YAML or MsgPack,
- validate data against a UCL/JSON schema.
It was heavily inspired by ucl (Ruby).
Requirements
This shard links against the native libucl library, which must be installed
on your system before you can compile or run anything.
The library is vendored as a git submodule (ext/libucl) and built from source.
You'll need the usual C build toolchain (build-essential automake autoconf libtool
on Debian/Ubuntu, or the equivalent on macOS).
# Fetch the vendored libucl source
git submodule update --init --recursive
# Build and install it into /usr/local/lib
mise libucl:build
mise libucl:install
These tasks are defined in
mise.toml. If you don't use mise, run the equivalent commands by hand:cd ext/libucl && ./autogen.sh && ./configure && make, then copysrc/.libs/libucl.*into/usr/local/lib.
Installation
Add the dependency to your shard.yml:
dependencies:
ucl:
github: jbox-web/ucl.cr
Then run shards install.
Usage
require "ucl"
Load
Parse a UCL (or JSON) string into native Crystal values:
UCL.load("foo = bar")
# => {"foo" => "bar"}
Load directly from a file (libucl resolves file variables and relative
includes, unlike UCL.load(File.read(path))):
UCL.load_file("config.conf")
Repeated keys always decode to arrays (scalars and objects):
UCL.load("s { a = 1 }\ns { b = 2 }")
# => {"s" => [{"a" => 1}, {"b" => 2}]}
For typed, cast-free access, use load_any — a JSON::Any-style wrapper:
cfg = UCL.load_any(%(server { port = 8080, hosts = ["a", "b"] }))
cfg["server"]["port"].as_i # => 8080
cfg["server"]["hosts"].as_a.map(&.as_s) # => ["a", "b"]
cfg["server"]["missing"]? # => nil
Dump
Serialize a Crystal object. The default emitter is config (UCL). Pick a format
with the typed UCL::Emitter enum (preferred) or the legacy string form —
json, json_compact, yaml, msgpack:
UCL.dump({"foo" => "bar"})
# => "foo = \"bar\";\n"
UCL.dump({"foo" => "bar"}, UCL::Emitter::Json)
# => "{\n \"foo\": \"bar\"\n}"
UCL.dump({"foo" => "bar"}, "json") # legacy string form still works
Hash keys must be strings; supported value types are String, Bool, Int,
Float, Time::Span, Nil, Array, Hash and NamedTuple. Anything else
raises UCL::Error::TypeError. A NamedTuple encodes like the equivalent
Hash, with its Symbol keys stringified:
UCL.dump({port: 8080, host: "localhost"})
# => "port = 8080;\nhost = \"localhost\";\n"
Each of those types also carries a #to_ucl instance method mirroring Crystal's
#to_json / #to_yaml, so you can dump inline. UCL::Value and UCL::Any
expose it too:
{"foo" => "bar"}.to_ucl # => "foo = \"bar\";\n"
{"foo" => "bar"}.to_ucl(UCL::Emitter::Json) # => "{\n \"foo\": \"bar\"\n}"
Hash#to_ucl checks its key type at compile time: a non-String key (e.g.
Hash(Int32, String)) is a compile error, not a runtime TypeError. Values are
not checked recursively — a nested unsupported value still raises
UCL::Error::TypeError at runtime.
Validate
Check data against a UCL/JSON schema. validate raises
UCL::Error::SchemaError on mismatch; valid? returns a boolean instead:
schema = File.read("schema.json")
data = File.read("data.json")
UCL.validate(schema, data) # => true, or raises UCL::Error::SchemaError
UCL.valid?(schema, data) # => true / false
Development
This project uses mise to manage the Crystal version and
common tasks (see mise.toml):
mise dev:deps # shards install
mise dev:spec # run the specs
mise dev:format # crystal tool format src/
mise dev:ameba # static analysis
The specs require the native libucl to be installed (see Requirements);
dev:spec sets LD_LIBRARY_PATH=/usr/local/lib for you.
Contributing
- Fork it (https://github.com/jbox-web/ucl.cr/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
This shard is released under the MIT License.
API
- Array(T)
An
Arrayis an ordered, integer-indexed collection of objects of type T. - Bool
Bool has only two possible values:
trueandfalse. - Float
Float is the base type of all floating point numbers.
- Hash(K, V)
A
Hashrepresents a collection of key-value mappings, similar to a dictionary. - Int
Int is the base type of all integer types.
- NamedTuple(**T)
A named tuple is a fixed-size, immutable, stack-allocated mapping of a fixed set of keys to values.
- Nil
The
Niltype has only one possible value:nil. - String
A
Stringrepresents an immutable sequence of UTF-8 characters. - UCL
Crystal bindings for LibUCL, a universal configuration language.