nette/neon: a lossless NEON parser that keeps every comment
🍸 Encodes and decodes NEON file format.
At a glance
- What is it?
- Nette's neon library reads the NEON configuration format into a concrete syntax tree where every character has a place, so a value can be changed without reformatting the file. It also ships an evaluator, an encoder and a linting tool, and it requires PHP 8.4 to 8.6.
- Who is it for?
- Adopt nette/neon when a PHP application has to modify NEON configuration files that humans also edit, since a lossless tree and the Evaluator are hard to reproduce with a plain decoder. Do not adopt it for JSON-only work, where the format is a subset and the extra machinery buys nothing, and do not adopt it on a runtime older than PHP 8.4, because that constraint alone rules it out.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 7, 2026, and from our analysis. They are not legal advice.
Star history
- nette/neon939 ★ · +0
| Repository | 2026-10-21 | 2026-11-07 |
|---|---|---|
| nette/neon | 939 | 939 |
0 Stars in the last 17 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Why a configuration file needs a syntax tree and not a dictionary
NEON is the human-friendly configuration format of the Nette framework, and the project describes it as a YAML alternative with entities. JSON is a subset of it, and the format is written up in a manual with a sandbox for trying it out. The problem this library addresses is narrower than parsing configuration, and that narrowness is the whole design. When a tool reads a config file into a PHP array and writes it back out, the comments are gone, the indentation is gone, the quoting style is gone, and every untouched line in the file shows up in the diff. That is the failure mode the library is built against: NEON is parsed into a concrete syntax tree in which every character of the file has its place, and printing that tree returns the file byte for byte.
That property is what makes automated edits safe, and it is what separates this from a format converter. The stated uses are configuration editors and admin panels, installers that register an extension in a config.neon file, migrations that rename a service, and agents that change configuration without wrecking it.
setValue rewrites the tokens it edits and nothing else
The editing API works on paths into the document, and each call changes only the tokens involved in the change. Setting a nested value, adding a key, adding an item to a list, and removing an item are separate operations with separate rules, and the details are where the value lies. A hexadecimal literal stays hexadecimal after a numeric edit. A boolean written as yes becomes no rather than the word false. Setting a value to the value it already has produces no change at all, so the file is not touched. A new key is indented like its siblings rather than in a style of the library's own choosing.
$document = Neon::parse($text);
$document->setValue(['database', 'port'], 3307); // 0x0CEA stays hexadecimal: 0x0CEB
$document->setValue(['database', 'debug'], false); // yes becomes no, not false
$document->setValue(['timeout'], 30); // the same value: no change at all
$document->setValue(['parameters', 'admin'], '[email protected]'); // a new key, indented like its siblings
$document->addItem(['services'], 'App\OrderFacade'); // a new item of a list
$document->removeItem(['database', 'driver']); // with the comment that describes it
$document->removeItem(['mail'], CommentPolicy::MoveToNextToken);
file_put_contents('config.neon', $document->print());The guarantees are described in terms of the specification: the file has to read back as the same tree, with the expected value, and with the change only where it was made. The project states that those guarantees are checked after every operation across a large corpus of edits.
A missing key in the path is an error, not a new section
Failure behaviour is defined rather than left to chance, and the interesting cases are the ones where a looser library would quietly do something. A missing key in the middle of a path is an error, so a typo in an installer does not create a stray section in the file. A refused edit changes nothing, not even half of itself, which matters when the edit is going to be written back to disk on the assumption that it succeeded. Removing the last item of a collection leaves the collection empty, rendered as an empty list, because in configuration an empty collection and a missing key often mean different things. An item marked nullable loses its value instead of being removed.
Removal also has a comment policy, and the choice is visible in the API. Removing an item can take the comment that describes it with it, or the comment can be moved to the next token, which is what the CommentPolicy::MoveToNextToken argument selects. Once the file is written, the diff for a change to one value is one line.
The tree keeps trivia attached to the tokens that carry it
Underneath the editing API sits a tree of eight node classes: DocumentNode, BlockArrayNode, InlineArrayNode, ArrayItemNode, EntityNode, EntityChainNode, StringNode and LiteralNode. Nodes have named slots, tokens carry their trivia, and trivia covers whitespace, line endings and comments. Because the comments are trivia on a token rather than a separate list somewhere, an item and the comment on its line stay together when the item is moved or removed.
The node API is what a tool like an editor plugin or an admin panel needs. You can find an item by path, ask for its value, read its trailing comments, ask for the text of its first token to get the value exactly as it was written rather than as it was evaluated, and ask for its position as a line, column and offset computed from the current tree. There are also find and findAncestor lookups, sibling access, and a Traverser for walking the tree with replacements.
$item = $document->findItem(['database', 'user']);
$item->getValue(); // 'root'
$item->getTrailingComments(); // the comment on its line
$item->value->getFirstToken()->text; // the text exactly as written
$item->getPosition(); // line, column and offset, computed from the current treeThe Evaluator and the encoder turn the tree into PHP values
Decoding is not a separate parser. It is the tree projected onto PHP values by a class called Evaluator, and the projection is a substitution point. A subclass can change how literals, strings, entities and items become values, while the keys and the order of the resulting arrays stay those of the document. That arrangement is how the DI container of Nette reads its configuration, and it means the mapping between NEON types and PHP types is a policy decision rather than a hardcoded table.
The default mapping is documented as a table. null, true and yes become null and bool. An integer becomes int, except that a value beyond the 64-bit range stays a string of its digits. A float becomes float, a string stays a string. A date becomes DateTimeImmutable, with the exception that an invalid date such as 2024-02-30 remains a string. An entity or an entity chain becomes a Nette\Neon\Entity, and a list or a map becomes an array. Errors throw Nette\Neon\Exception carrying the position of the problem, so a parser failure points at a place in the file rather than at a stack frame.
The encoder can write NEON in the inline style or in the block style, and the choice is either a flag or a callback. The default produces compact inline output such as a single mapping with an inline list, while the block form writes a key on its own line with tab-indented list items. Passing a callback lets the caller decide per value: the example in the documentation encodes blocks for the first level only, which keeps a top-level structure readable while nested values stay compact.
use Nette\Neon\Neon;
$value = Neon::decode('hello: world'); // ['hello' => 'world']
$value = Neon::decodeFile('config.neon');
$neon = Neon::encode(['a' => [1, 2]]); // '{a: [1, 2]}'
$neon = Neon::encode(['a' => [1, 2]], true); // "a:\n\t- 1\n\t- 2\n\n"
$neon = Neon::encode($config, fn($v, $path) => !$path); // blocks for the first level onlyThe other half of the encoder is what it refuses to do. Values that cannot be written without loss are errors, and the listed cases are invalid UTF-8, INF and a cycle. The error names the path to the offending value, so a nested failure is locatable in a large structure rather than reported as a generic failure. For a configuration format that has to survive round trips, refusing to guess is the right default.
Installing it takes one Composer line and PHP 8.4 to 8.6
The library is installed as a Composer package, and the runtime requirement is stated plainly.
composer require nette/neonIt requires PHP 8.4 up to 8.6 with the json and mbstring extensions. That is a narrow window, and it is the first thing to check against your deployment before anything else, because the package will not install on an older runtime. The mbstring requirement is worth noting separately from json since it is an extension that has to be present in the image, not just in the language.
The repository ships runnable example directories covering edge cases, parsing, encoding, editing, config editing, the evaluator, linting, the tree and a verify script, so the behaviour of each API is demonstrable without writing a project first. There is also a neon-lint command for editors, CI and agents, which is the natural companion to a library whose main argument is that a file's formatting should be preserved.
A plain decoder is the alternative, and it loses the file
The realistic alternative to this library is not another NEON implementation, it is the general approach of parsing a configuration file into plain values and serialising it back. Any YAML or JSON reader can be pointed at a NEON file that sticks to the JSON subset, and for a file that only ever holds JSON-compatible values, that is enough. The difference in approach is that a plain reader throws away the document and keeps the data, while a concrete syntax tree keeps the document and derives the data from it. Everything this library adds follows from that: token trivia, positions, the fact that a value is written as 0x0CEA rather than 3306, the ability to remove an item and take its comment with it.
The other alternative is to leave configuration files to human editors and automate around them instead, letting a script locate a key with a regular expression or a line scan and patch the text directly. That approach is common and it is also what produces the diffs this library exists to avoid, since a text patch has to guess at indentation and can silently miss a second occurrence of the same key. For a file nobody edits but the application, both approaches work. For a file that a person reviews in every pull request, the tree is the difference between a one line diff and a rewritten file.
New BSD terms, a custom licence field and a busy release line
The repository is not archived and the last push was on 2026-10-18, so the code is being worked on. The release line is regular by the record: v3.4.8 on 2026-05-11, v3.4.7 on 2026-01-04 and v3.4.6 on 2025-11-25, all on the 3.4 series.
On the licence, the licence field on the repository says it is a custom licence GitHub cannot classify, with a pointer to the LICENSE file. The project itself labels the licence New BSD in its badge, and the repository contains a license.md file at the root, so the terms to read are the ones in that file rather than a canonical licence identifier. In practice the New BSD grant is permissive and allows use in closed source products, but the notice and licence text have to be kept with copies of the code. Anyone shipping this inside a commercial product should read license.md directly, because the classifier on the repository page does not tell you which variant is in force.
The upgrade cost is mostly the PHP constraint. Moving a project onto a newer runtime is a prerequisite for taking a current release of this library, not an optional step.
Editorial conclusion
Adopt nette/neon when a PHP application has to modify NEON configuration files that humans also edit, since a lossless tree and the Evaluator are hard to reproduce with a plain decoder. Do not adopt it for JSON-only work, where the format is a subset and the extra machinery buys nothing, and do not adopt it on a runtime older than PHP 8.4, because that constraint alone rules it out. Before committing, check the composer require nette/neon line against the PHP version your deployment actually runs, and read the LICENSE file in the repository root, because the licence field on the repository says it is a custom licence GitHub cannot classify.
Frequently asked questions
What PHP version does nette/neon require?
It requires PHP 8.4 up to 8.6 with the json and mbstring extensions. The package will not install on older runtimes, so check your deployment version before adding it.
How do I change a value in a NEON file without losing comments and formatting?
Parse the file into a document, call setValue with a path such as array('database', 'host'), and print the document back. The library states that printing the tree returns the original text byte for byte, and that every edit changes only the tokens it edits.
Is NEON compatible with JSON?
JSON is a subset of NEON, and the format description and a sandbox are linked from the project page. NEON adds inline arrays, entities with attributes, multiline strings without escaping and comments, none of which JSON has.
What does the neon-lint command do?
It is a linting tool for editors, CI and agents, listed as a feature of the project. The repository ships it under a bin directory alongside the library source and an examples directory that includes a linting example.
Official sources
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.
[](https://hysenlabs.com/projects/nette-neon)Star history chart for your README
[](https://hysenlabs.com/projects/nette-neon)Was this useful?