Skip to content

The JSON document model

The Rivoli PDF DOM serializes cleanly to and from JSON. Because the model is plain data, the JSON is the document: you can store it, version it, diff it, cache it, and reconstruct the exact same tree later. This page covers the serialization modes, why the output is deterministic, and how node identity survives the round trip.

The serialization helpers are extension methods in Rivoli.Pdf.Serialization.

using Rivoli.Pdf;
using Rivoli.Pdf.Serialization;
var doc = Document.Create("Annual Report", "Finance");
doc.AddSection().Add(new Paragraph("Executive summary"));
string json = doc.ToJsonCanonical(); // Document -> JSON
Document copy = JsonSerialization.FromJson(json); // JSON -> Document

FromJson reconstructs the full tree, including every node’s identity (see below). There are file-based variants too: ToJsonFile(path), ToJsonFileCanonical(path), ToJsonFileCompact(path), and FromJsonFile(path).

All three modes preserve every bit of data: they differ only in formatting and size.

ModeMethodBest for
DefaultToJson()General use, debugging
CanonicalToJsonCanonical()Version control, document comparison
CompactToJsonCompact()Cache storage, network transmission
var pretty = doc.ToJson(); // indented, camelCase, nulls omitted
var git = doc.ToJsonCanonical(); // deterministic, human-readable
var forCache = doc.ToJsonCompact(); // minified, ~30–50% smaller

Indented, camelCase property names, with null values omitted. Good for reading and debugging.

The format to commit to Git. It is deterministic: serializing the same document twice produces byte-identical output.

var a = doc.ToJsonCanonical();
var b = doc.ToJsonCanonical();
// a == b, always

Determinism comes from a few rules:

  • Properties are written in a stable order.
  • Collections preserve insertion order.
  • Enums serialize as consistent camelCase strings.
  • Null values are omitted, removing a source of variation.

Byte-identical output means clean, reviewable diffs and reliable document-equality checks.

Same data, no whitespace: typically 30–50% smaller. Use it for Redis/caches and network payloads, where humans aren’t reading the bytes. It is still deterministic.

Block and inline types are distinguished by a $type discriminator, so the polymorphic tree round-trips faithfully:

{
"id": "abc-123",
"metadata": { "title": "Annual Report", "author": "Finance" },
"sections": [
{
"id": "section-1",
"blocks": [
{
"$type": "paragraph",
"id": "para-1",
"text": "Content"
}
]
}
]
}

The discriminator values match the concrete types: paragraph, list, table, imageBlock, pageBreak, containerBlock for blocks; run, hyperlink, span, inlineImage, inlineSymbol for inlines.

Every node carries an id (a GUID string), and that identity is preserved by serialization. Serialize, deserialize, and each node keeps the same id: the tree that comes back is identical to the one that went out, down to node identity.

var original = Document.Create("Doc");
var paraId = ((Paragraph)original.AddSection().Add(new Paragraph("Hi")).Blocks[0]).Id;
var restored = JsonSerialization.FromJson(original.ToJsonCanonical());
// the restored paragraph has the same Id as paraId

This is what enables:

  • Git-friendly history: a change to one paragraph shows up as a change to that paragraph’s object, not a wholesale rewrite, because everything else keeps its identity and order.
  • Caching keyed on identity.
  • Collaborative editing and diff/patch: two versions of “the same document” can be aligned node-by-node.

See Node identity for how identity is generated and why it’s immutable.

Most PDF libraries treat the document as a write-once pipeline: you build it, you emit bytes, and the structure is gone. Rivoli PDF keeps the structured model as a first-class, serializable artifact. That unlocks server-side editing (load JSON, mutate the tree, save JSON), caching of expensive-to-build documents, deterministic comparison in tests, and a clean audit trail: all without re-deriving structure from rendered PDF.