A "no badge / not found" state on any of the registry badges above means the corresponding package has not been published yet for that version. The badges read live from the package registries, so they always reflect the latest published state.
Universal serialization library to encode/decode objects to/from Links Notation format. Available in Python, JavaScript, Rust, and C# with identical functionality and API design.
This library provides universal serialization and deserialization with built-in support for circular references and complex object graphs in:
- Python - Full implementation for Python 3.8+
- JavaScript - Full implementation for Node.js 18+
- Rust - Full implementation for Rust 1.70+
- C# - Full implementation for .NET 8.0+
All implementations share the same design philosophy and provide feature parity.
- Universal Serialization: Encode objects to Links Notation format
- Type Support: Handle all common types in each language:
- Python:
None,bool,int,float,str,list,dict - JavaScript:
null,undefined,boolean,number,string,Array,Object - Rust:
LinoValueenum withNull,Bool,Int,Float,String,Array,Object - C#:
null,bool,int,long,float,double,string,List<object?>,Dictionary<string, object?> - Special float/number values:
NaN,Infinity,-Infinity
- Python:
- Readable by Default: In every language
encode()writes indented, plain-text Links Notation; the previous single-line base64 form stays available asencode_compact()(aliasencode_obfuscated()) - Object Identity: Shared references and circular references are preserved by the compact format via object ids; the readable format is a plain tree and raises a circular-reference error instead
- Full Unicode: Strings are written as text; only a value that cannot be written as text (one holding control characters) is base64-encoded, and it is marked individually as
(base64 "…") - Opt-in Tracing: Set
LINO_CODEC_DEBUG=1to trace encoding and decoding, the same way in every language - Simple API: Easy-to-use
encode()anddecode()functions - JSON/Lino Conversion: Convert between JSON and Links Notation (JavaScript)
- Reference Escaping: Properly escape strings for Links Notation format (JavaScript)
- Fuzzy Matching: String similarity utilities for finding matches (JavaScript)
- Indented Format: Human-readable indented Links Notation format for display and debugging
pip install lino-objects-codecfrom link_notation_objects_codec import encode, decode
# Encode and decode
data = {"name": "Alice", "age": 30, "active": True}
encoded = encode(data)
decoded = decode(encoded)
assert decoded == datanpm install lino-objects-codecimport { encode, decode } from "lino-objects-codec";
// `encode` produces readable, indented Links Notation by default
const data = { name: "Alice", age: 30, active: true };
const encoded = encode({ obj: data });
const decoded = decode({ notation: encoded });
console.log(JSON.stringify(decoded) === JSON.stringify(data)); // true[dependencies]
lino-objects-codec = "0.1"use lino_objects_codec::{encode, decode, LinoValue};
// Encode and decode
let data = LinoValue::object([
("name", LinoValue::String("Alice".to_string())),
("age", LinoValue::Int(30)),
("active", LinoValue::Bool(true)),
]);
// `encode` produces readable, indented Links Notation
let encoded = encode(&data);
assert_eq!(encoded, "(\n name \"Alice\"\n age 30\n active true\n)");
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded, data);(
name "Alice"
age 30
active true
)
The single-line base64 form is still available as encode_compact() (alias
encode_obfuscated()) in every language, and decode() accepts both forms.
dotnet add package Lino.Objects.Codecusing Lino.Objects.Codec;
// Encode and decode
var data = new Dictionary<string, object?>
{
{ "name", "Alice" },
{ "age", 30 },
{ "active", true }
};
var encoded = Codec.Encode(data);
var decoded = Codec.Decode(encoded) as Dictionary<string, object?>;
Console.WriteLine(decoded?["name"]); // Alice.
├── python/ # Python implementation
│ ├── src/ # Source code
│ ├── tests/ # Test suite
│ ├── examples/ # Usage examples
│ └── README.md # Python-specific docs
├── js/ # JavaScript implementation
│ ├── src/ # Source code
│ ├── tests/ # Test suite
│ ├── examples/ # Usage examples
│ └── README.md # JavaScript-specific docs
├── rust/ # Rust implementation
│ ├── src/ # Source code
│ ├── examples/ # Usage examples
│ └── README.md # Rust-specific docs
├── csharp/ # C# implementation
│ ├── src/ # Source code
│ ├── tests/ # Test suite
│ ├── examples/ # Usage examples
│ └── README.md # C#-specific docs
└── README.md # This file
For detailed documentation, API reference, and examples, see:
All implementations support the same features with language-appropriate syntax:
Object identity -- shared nodes and cycles -- is a property of the compact
format, which names shared nodes with obj_N ids. The readable format is a plain
tree with nowhere to put those ids, so encode() raises a circular-reference
error on a cycle; use encode_compact() (the compact form) when you need
identity preserved.
Python:
from link_notation_objects_codec import decode, encode_compact
# Self-referencing list -- preserved by the compact format
lst = [1, 2, 3]
lst.append(lst)
decoded = decode(encode_compact(lst))
assert decoded[3] is decoded # Reference preservedJavaScript:
import { encodeCompact, decode } from "lino-objects-codec";
// Self-referencing array -- preserved by the compact format
const arr = [1, 2, 3];
arr.push(arr);
const decoded = decode({ notation: encodeCompact({ obj: arr }) });
console.log(decoded[3] === decoded); // true - Reference preservedRust:
use lino_objects_codec::{encode_compact, decode, LinoValue};
// Self-referencing structures are handled via object ids in the compact form
let data = LinoValue::array([LinoValue::Int(1), LinoValue::Int(2)]);
let decoded = decode(&encode_compact(&data)).unwrap();
// Reference semantics preserved through encoding/decodingC#:
using Lino.Objects.Codec;
// Self-referencing list -- preserved by the compact format
var lst = new List<object?>();
lst.Add(lst);
var decoded = Codec.Decode(Codec.EncodeCompact(lst)) as List<object?>;
Console.WriteLine(ReferenceEquals(decoded, decoded?[0])); // True - Reference preservedPython:
data = {
"users": [
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"}
],
"metadata": {"version": 1, "count": 2}
}
assert decode(encode(data)) == dataJavaScript:
const data = {
users: [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
],
metadata: { version: 1, count: 2 },
};
console.log(JSON.stringify(decode(encode(data))) === JSON.stringify(data));Rust:
use lino_objects_codec::{encode, decode, LinoValue};
let data = LinoValue::object([
("users", LinoValue::array([
LinoValue::object([("id", LinoValue::Int(1)), ("name", LinoValue::String("Alice".to_string()))]),
LinoValue::object([("id", LinoValue::Int(2)), ("name", LinoValue::String("Bob".to_string()))]),
])),
("metadata", LinoValue::object([
("version", LinoValue::Int(1)),
("count", LinoValue::Int(2)),
])),
]);
assert_eq!(decode(&encode(&data)).unwrap(), data);C#:
var data = new Dictionary<string, object?>
{
{
"users", new List<object?>
{
new Dictionary<string, object?> { { "id", 1 }, { "name", "Alice" } },
new Dictionary<string, object?> { { "id", 2 }, { "name", "Bob" } }
}
},
{ "metadata", new Dictionary<string, object?> { { "version", 1 }, { "count", 2 } } }
};
var decoded = Codec.Decode(Codec.Encode(data));The indented format provides a human-readable representation for displaying objects:
JavaScript:
import { formatIndented, parseIndented } from "lino-objects-codec";
// Format an object with an identifier
const formatted = formatIndented({
id: "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019",
obj: {
uuid: "6dcf4c1b-ff3f-482c-95ab-711ea7d1b019",
status: "executed",
command: "echo test",
exitCode: "0",
},
});
console.log(formatted);
// Output:
// 6dcf4c1b-ff3f-482c-95ab-711ea7d1b019:
// uuid '6dcf4c1b-ff3f-482c-95ab-711ea7d1b019'
// status executed
// command 'echo test'
// exitCode '0'
// Parse it back
const { id, obj } = parseIndented({ text: formatted });Python:
from link_notation_objects_codec import format_indented, parse_indented
# Format an object with an identifier
formatted = format_indented(
'6dcf4c1b-ff3f-482c-95ab-711ea7d1b019',
{'uuid': '6dcf4c1b-ff3f-482c-95ab-711ea7d1b019', 'status': 'executed'}
)
# Parse it back
id, obj = parse_indented(formatted)Rust:
use lino_objects_codec::format::{format_indented_ordered, parse_indented};
// Format an object with an identifier
let pairs = [("status", "executed"), ("exitCode", "0")];
let formatted = format_indented_ordered("my-uuid", &pairs, " ").unwrap();
// Parse it back
let (id, obj) = parse_indented(&formatted).unwrap();C#:
using Lino.Objects.Codec;
// Format an object with an identifier
var obj = new Dictionary<string, string?> { { "status", "executed" }, { "exitCode", "0" } };
var formatted = Format.FormatIndented("my-uuid", obj);
// Parse it back
var (id, parsedObj) = Format.ParseIndented(formatted);The library uses the links-notation format as the serialization target. Each object is encoded as a Link with type information:
In every language encode() writes one ( ) construct for both objects and
arrays, at every level including the root. Lines of the form key value make an
object, bare-value lines make an array:
(
name "Alice"
age 30
active true
)
- Strings are double-quoted and written as text; numbers,
true,falseandnullare bare, so types survive a round trip NaN,Infinityand-Infinityare written as such- An empty array is
(); an empty object is(+ newline +) - Only a value that cannot be written as text (one containing control characters)
is base64-encoded, and it is marked individually as
(base64 "bGluZTEKbGluZTI=") - The four languages produce byte-identical output, checked by the shared
fixtures in
fixtures/readable-format/cases.json
The previous single-line form, kept for compatibility and for the object graphs the readable tree cannot express (shared and circular references):
- Basic types are encoded with type markers:
(int 42),(str aGVsbG8=),(bool true) - Strings are base64-encoded to handle special characters and newlines
- Collections with self-references use
(obj_id: type content...), e.g.(obj_0: dict ((str c2VsZg==) obj_0))for{"self": obj} - Circular references use direct object id references:
obj_0(without arefkeyword)
decode() detects which of the two forms it is given, so previously written
files keep decoding, and every language reads the compact documents the others
write.
Tracing is off by default and can be turned on in any language by setting the
LINO_CODEC_DEBUG environment variable to a truthy value (1, true, yes or
on), or from code (set_debug_enabled / setDebugEnabled /
CodecDebug.SetEnabled). Trace lines go to standard error, prefixed with
[lino-codec].
cd python
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest tests/ -vcd js
npm install
npm test
npm run examplecd rust
cargo test
cargo run --example basic_usagecd csharp
dotnet build
dotnet test
dotnet run --project examples/BasicUsage.csprojContributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Add tests for your changes
- Ensure all tests pass
- Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the Unlicense - see the LICENSE file for details.
- GitHub Repository
- Links Notation Specification
- PyPI Package (Python)
- npm Package (JavaScript)
- crates.io Package (Rust)
- NuGet Package (C#)
This project is built on top of the links-notation library.