Library / SDK
vstakhov/libucl avatar
vstakhov/libucl

libucl: Parsing UCL, JSON, MessagePack, and CBOR in C

Universal configuration library parser

1,747 stars165 forksCBSD-2-Clause

At a glance

What is it?
libucl is a BSD-2-Clause C library for human-editable UCL configuration and several structured data formats. Its broad format support is useful, but its safe-parser limits and incompatible object mappings require careful adoption decisions.
Who is it for?
Adopt libucl when a C application needs editable UCL configuration plus controlled access to several data formats. Do not enable macros or includes for untrusted input, and verify parser limits, binary-format mappings, and lossless round trips before relying on it in production.
Can I use it commercially?
Yes. BSD-2-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly C, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What libucl parses and who should use it

libucl addresses the gap between strict JSON and the configuration syntax that administrators often prefer to edit by hand. UCL permits unquoted strings and keys, allows `=` instead of `:`, can omit braces around a top-level object, and accepts commas or semicolons as separators. Repeated keys can be represented internally as implicit arrays, which makes it practical to write configuration that resembles nginx syntax while retaining JSON compatibility. The intended users are C developers building applications, servers, or system tools that need a parser for configuration files and interchange formats.

The library also handles JSON, MessagePack, CBOR, and canonical S-Expressions. It can read and write JSON, MessagePack, and CBOR. YAML support is described as limited and mainly intended for compatibility, while S-Expressions are read only. This makes libucl broader than a configuration-only parser, but the extra formats are not equivalent representations. Their handling of roots, tags, byte strings, repeated keys, and integer keys is constrained by the UCL object model.

The repository includes C and C++ examples, Lua bindings, Python and Haskell directories, tests, and multiple build files. The documented API is in `doc/api.md`, while the example at `examples/ucl_cpp.cc` gives a concrete integration reference. The project is not archived, and the last push was on 2026-10-07. Recent releases include 0.9.4 on 2026-02-17, 0.9.3 on 2025-12-09, and 0.9.2 on 2024-04-20. The repository data does not establish a support policy for older releases, so version compatibility should be checked against the selected API.

UCL syntax and its automatic object transformations

UCL is designed to reduce punctuation in editable configuration. A top-level key-value pair can be written without enclosing braces, keys and strings do not always require quotes, and object separators can be flexible. The README shows `key = value;` and a `section { ... }` block as equivalent to a quoted JSON object containing string keys.

Repeated object keys are a central feature rather than a syntax error. If a key appears more than once, libucl automatically creates an array internally. This is useful for repeated nested structures such as multiple host definitions, but it changes the object model seen by callers. Code that retrieves one value by key must account for the possibility of an implicit array, especially when the same source document is also processed as JSON.

UCL adds conveniences beyond basic JSON notation. The documentation lists comments, macros, variables, multiline strings, single-quoted strings, automatic array creation, and convenient numbers and booleans. Numbers can include unit-style forms such as `10k`, and time values such as `0.2s` are represented in the README's JSON comparison as the string `"0.2s"`. This means application code should not assume that every human-friendly scalar becomes a numeric JSON value.

The parser's format detection also needs deliberate selection. `UCL_PARSE_AUTO` recognizes MessagePack and other supported formats where their signatures permit it, but CBOR input must be requested with `UCL_PARSE_CBOR` unless the document carries the self-described CBOR tag `d9 d9 f7`. The README specifically warns that CBOR and MessagePack head bytes overlap, so automatic detection is not a substitute for knowing the input format.

Security controls for configuration parsing

The most important adoption constraint is the trust model. With all features enabled, libucl is intended for trusted configuration controlled by a system administrator. The README states that it is not designed to parse untrusted or adversarial input safely. `.include` directives can read arbitrary files, macros can trigger external actions, and URL includes can make network requests. Those capabilities are useful for controlled configuration management, but they turn parser input into filesystem or network activity.

For untrusted input, the documented alternatives are to use pure JSON mode without macros, or to disable dangerous features such as macro handlers and URL includes. The parser also supports a safety-oriented construction with `UCL_PARSER_SAFE_FLAGS`, which excludes macros, file variables, time suffixes, and implicit arrays. Structural limits can then be applied with `ucl_parser_set_limits()`. The README provides this configuration example:

c
struct ucl_parser_limits limits = {
	.max_depth = 64,
	.max_nodes = 1000000,
	.max_alloc = 64 * 1024 * 1024,
	.max_key_length = 1024,
	.max_string_length = 16 * 1024 * 1024,
};
struct ucl_parser *parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS);

ucl_parser_set_limits(parser, &limits);

A zero limit means no limit for that field. Only `max_depth` has a default value of 1024, because the other budgets could reject large valid documents. Exceeding a limit produces `UCL_ELIMIT`, while excessive nesting produces `UCL_ENESTED`. These limits apply to UCL, JSON, MessagePack, and CBOR parsers. The README warns that binary formats can amplify structural cost more sharply than text because a complete container may cost only one byte in the input. The Lua API offers `ucl.untrusted_parser([limits])`, plus `parser:set_limits()` and `parser:get_limits()`.

Binary formats and lossy object-model conversions

libucl's binary-format support is useful, but conversion into UCL objects is not always lossless. CBOR requires the top-level item to be an array or map, as MessagePack does. CBOR indefinite-length strings, arrays, and maps are supported, and chunks of an indefinite string are concatenated. Tags are dropped, leaving the wrapped item in their place.

CBOR map keys may be text strings, byte strings, or integers. An integer key is converted to its decimal spelling because UCL object keys are strings. Byte strings become UCL strings marked with `UCL_OBJECT_BINARY`, which allows the emitter to write them back as byte strings. Simple values other than `false`, `true`, and `null` become a null object, while integers outside the `int64_t` range are rejected.

The most serious documented conversion problem is repeated keys. Because a CBOR map declares a fixed number of pairs, a key holding an implicit array contributes only its first value when the tree is written back out. The remaining values are dropped. The same restriction applies to MessagePack output. A parse-and-emit cycle therefore cannot be assumed to preserve duplicate-key intent in those formats. A document that also has a root object already held by the parser will be rejected when a second top-level document is introduced.

These rules make libucl a convenient interoperability layer, but not a neutral format-preserving validator. An application should test the exact combination of input format, parser flags, output format, and object access pattern it plans to use.

Installation and first C integration

The README does not provide installation commands, configure flags, package-manager instructions, or a minimal application program. It points readers to the API documentation at `doc/api.md`, and the repository layout shows `CMakeLists.txt`, `Makefile.am`, `Makefile.unix`, `Makefile.w32`, `configure.ac`, and `autogen.sh`. Those files indicate that the project supports several build contexts, but the README does not document the exact command to use for any one of them. An installation tutorial with invented flags or build steps would therefore be misleading.

The first integration should begin by selecting the parser flags for the actual input source. For ordinary controlled configuration, the full UCL feature set may be appropriate. For untrusted input, the documented path is `UCL_PARSER_SAFE_FLAGS`, followed by `ucl_parser_set_limits()`. Applications should also choose `UCL_PARSE_CBOR` for CBOR when the input is not prefixed with the self-described tag.

The repository includes `examples/ucl_cpp.cc`, but the README does not reproduce its contents. It is therefore safest to use the API reference and that example as the starting points, then verify the actual call sequence against the selected release. No claims about a particular API return value, object traversal helper, or emitter invocation can be made from the README alone.

libucl versus a format-specific parser

A format-specific parser can offer a narrower and more predictable model when an application only needs JSON or only needs CBOR. libucl's distinguishing approach is to parse several formats into one UCL object tree and then emit UCL, JSON, MessagePack, CBOR, or limited YAML. That design is attractive when the same configuration data must cross format boundaries or when human-edited UCL is a requirement.

The trade-off is that the common tree imposes limits. CBOR tags disappear, integer keys become decimal strings, unsupported simple values become null, and implicit arrays lose additional values during binary output. S-Expressions can be read but not written. YAML output is limited. A dedicated parser may represent these formats according to their own native rules, whereas libucl favors a shared configuration-oriented model. The choice depends on whether interoperability and UCL syntax matter more than exact preservation of each source format.

License is BSD-2-Clause according to the repository metadata. That is a permissive license category, but it does not by itself answer questions about notices, dependencies, or distribution obligations. The repository contains `COPYING`, `libucl.pc.in`, and multiple language bindings, so an adopter should inspect the actual files shipped by the chosen build.

Editorial conclusion

Adopt libucl when a C application needs editable UCL configuration plus controlled access to several data formats. Do not enable macros or includes for untrusted input, and verify parser limits, binary-format mappings, and lossless round trips before relying on it in production. The first integration check should be a representative document parsed with the exact parser flags and limits required by the application.

Frequently asked questions

Can libucl parse JSON as well as UCL?

Yes. The README states that libucl can read and write JSON, and that UCL is compatible with JSON notation. Pure JSON mode without macros is also one of the documented options for handling untrusted input.

Is libucl safe for untrusted configuration input?

Not with all features enabled. The README says macros, file includes, and URL includes can perform filesystem or network actions. It recommends safe flags, disabled dangerous features, and explicit parser limits for untrusted input.

How do I parse CBOR input with libucl?

Request CBOR explicitly with `UCL_PARSE_CBOR`. Automatic detection can recognize it when the document starts with the self-described CBOR tag `d9 d9 f7`, because CBOR and MessagePack header bytes overlap.

Official sources

  1. Issues
  2. License: BSD-2-Clause
  3. README
  4. Releases
  5. vstakhov/libucl on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vstakhov-libucl.svg)](https://hysenlabs.com/projects/vstakhov-libucl)

Was this useful?