A minimal key = value configuration parser for Dart and Flutter.
Flat, human-friendly
key = valueconfiguration β inspired by π» Ghostty, simpler than INI or TOML.
Configuration files people edit by hand should be boring to read and impossible
to misread. flatconfig keeps the whole format at key = value: one flat
namespace, no sections, no nesting, no parser magic. Every line means what it
says.
It is built for tools, CLIs and Flutter apps that need structured settings without a heavy dependency β with duplicate keys, comments, explicit resets, recursive includes and lossless round-tripping.
- π§© Tiny syntax β
key = value, quoted when it has to be - π¦ Pure Dart, two small dependencies (
meta, andpathfor includes) - π Web and WASM safe, with file I/O in a library you simply do not import
- π Duplicate keys preserved, in order β which is how overriding works
- π Strict or lenient, with one
onIssuechannel for every problem - β Valid by construction β an entry the format cannot write out cannot be built
- π§ One accessor rule β
getX,getXOr,requireX, for every type - π
getAsfor everything else β your converter, the same three shapes - π Round-trips, including the values that look like they should break it
- π Includes from disk, memory, assets or the network, through a resolver
dart pub add flatconfigimport 'package:flatconfig/flatconfig.dart';The package is four libraries. Each of the three below re-exports the core, so one import usually does.
| Import | Adds to the core | Web / WASM |
|---|---|---|
package:flatconfig/flatconfig.dart |
the core: documents, parsing, encoding, accessors | β |
package:flatconfig/flatconfig_includes.dart |
following config-file through a resolver |
β |
package:flatconfig/flatconfig_accessors.dart |
DateTime, Duration, Uri, JSON, enum accessors |
β |
package:flatconfig/flatconfig_io.dart |
reading and writing files | β needs dart:io |
Only flatconfig_io.dart touches dart:io, so a browser build simply does not
import it. There is no stub that compiles and then throws at runtime.
import 'package:flatconfig/flatconfig.dart';
void main() {
const raw = '''
# Example configuration
background = 343028
foreground = f3d735
shader = bloom
shader = vignette
texture =
''';
final doc = FlatDocument.parse(raw);
print(doc['background']); // 343028
print(doc['shader']); // vignette β the last write wins
print(doc.allValues('shader')); // [bloom, vignette]
print(doc['texture']); // null
print(doc.lookup('texture')); // FlatLookup.reset() β cleared on purpose
}Typed reads follow one rule: getX returns null, getXOr returns your
fallback, requireX throws.
final port = doc.getIntOr('port', 8080);
final gamma = doc.requireDouble('gamma');
final features = doc.getList('features'); // "a, b, c" β [a, b, c]
final accent = doc.getAs('accent', parseMyColor); // anything elseFiles hang off File, so a path is spelled the way it is everywhere else in
Dart:
import 'dart:io';
import 'package:flatconfig/flatconfig_io.dart';
final doc = await File('main.conf').parseWithIncludes();
await File('out.conf').writeFlat(doc.withValue('font-size', '16'));# Comments start with "#" and take a whole line.
# Whitespace around "=" is ignored.
background = 343028
# Quotes preserve spaces, "=" and "#". They are not inline comments:
# this format has whole-line comments only.
font-family = "FiraCode Nerd Font"
# A key may appear more than once. The last one wins,
# and every value stays readable through allValues().
shader = bloom
shader = vignette
# An empty value is an explicit reset, not an empty string.
texture =
empty = ""
# Pull in another file. Later includes win; "?" marks it optional.
config-file = theme.conf
config-file = ?user.confKeys are case-sensitive. The comment prefix is configurable; the = separator
is not. SPEC.md is the normative definition, and its Appendix A
tracks where the implementation still deviates from it.
| Feature | INI / TOML | flatconfig |
|---|---|---|
| Sections / tables | β
[section] |
π« one flat namespace |
| Nested data | β tables or dotted keys | π« flat, but fromData flattens for you |
| Comments | # or ; |
# only, whole-line |
| Lists | β
[1, 2, 3] |
β
getList(), or repeat the key |
| Types | explicit | strings plus typed accessors |
| Includes | β (TOML: preprocessors only) | β built in, recursive, pluggable |
| Duplicate keys | β usually an error | β the mechanism for overriding |
Leaving out sections is the point rather than a shortcut: a single flat namespace makes merging, overriding and diffing trivial, and keeps the files readable for people who do not write code.
Think of it as the portable 20% of INI that covers 90% of real configuration.
| Page | What is in it |
|---|---|
| Parsing | entry points, options, strict vs lenient, reporting problems |
| The document model | entries and the resolved view, the three states, editing, collapse, prefixes |
| Accessors | the three shapes, custom converters, the optional accessors |
| Includes | config-file, merge policies, writing a resolver |
| Building documents | from maps, the environment and nested data; encoding and round-tripping |
| Files and platforms | the File API, and what runs on the web |
| Migrating from 0.5.x | the complete rename and removal table |
| Working on flatconfig | tests, coverage, CI, house rules |
SPEC.md |
the format, normatively |
Runnable examples live in example/, including a Flutter app in
example/flatconfig_flutter.
Flat, simple, predictable. No nested scopes, no hidden semantics, no parser magic. The goal is not to replace JSON, YAML or TOML, but to be the lightweight middle ground: friendly enough to hand-edit, strict enough to automate.
Where that forces a choice, it is resolved towards the reader of the file. A malformed line is reported rather than swallowed, a value that cannot survive a round trip is refused when it is written rather than mangled when it is read, and a key that was cleared on purpose is distinguishable from one nobody ever mentioned.
flatconfig keeps your config files boring β in the best possible way. π
Made with β€οΈ in Dart β grumpypixel/flatconfig