Catalog · How the catalog works
json4d
- ★ 1 on GitHub
Anyone may press Interesting, signed in or not. It helps others find this project.
README
Json4D
High-performance JSON manipulation for Delphi 12 — fluent API, indexed path lookup, compiled paths, null-safe reads, a dynamic Variant accessor, and a typed interface-proxy API powered by RTTI + custom attributes.
Table of Contents
- Why Json4D?
- Features
- Installation
- Quick Start
- The Four APIs
- Reading Data
- Writing Data
- Dynamic Variant API
- Typed Interface API
- The
TJson4DComponent - Comparison with
System.JSON - Performance
- Architecture
- Roadmap
- Documentation
- Contributing
- License
Why Json4D?
Delphi's built-in System.JSON works, but real codebases pay a price for it: noisy TJSONObject casts, repeated tree walks, error-prone manual conversions, and zero IntelliSense over your data shape. Json4D is a thin, opinionated layer above System.JSON that focuses on three things:
- Reads are fast. Every parse builds an in-memory path index, so subsequent lookups are O(1) dictionary reads — no recursive tree scans.
- Reads are safe. Missing keys return null-safe wrappers with defaults; deep
Json['a']['b']['c']chains never raise on absent intermediates. - Reads can be typed. Declare an interface, get IntelliSense, and let
TJson.Deserialize<IClient>(json)return something that behaves exactly likeIClient— including nested interfaces andTArray<T>— without a single line of boilerplate mapping.
If your project does a lot of JSON I/O — REST clients, configuration files, message queues, integrations — Json4D pays for itself on the first dozen endpoints.
Features
- Fluent
TJsonValueAPI with bracket access (Json['key'][0]) and chain-safe null handling. - Path index built at parse time — O(1) average lookup, no tree traversal.
- Compiled paths — pre-computed text + hash for hot-path repeated reads.
- Bracket setter with auto-create —
Json['a']['b']['c'] := 'x'builds missing parent objects automatically. - Fluent builder —
Add/AddObj/AddArray/EndObj/EndArrayfor object and array contexts. - Dynamic
VariantAPI —TJson.ParseVariantfor JavaScript-styleJsonVar.user.namedot access (read and write). - Typed interface API —
TJson.Deserialize<T>/TJson.Serialize<T>backed byTVirtualInterfaceproxies. - RTTI custom attributes —
[JsonName],[JsonIgnore],[JsonRequired],[JsonFormat],[JsonDefault]. TJson4Dnon-visual component for theORDatapalette — design-time JSON without a line of code.- Clean Architecture layering — Core / Contracts / Infrastructure / Presentation, dependencies pointing inward.
- DUnitX test suite covering parse, builder, path index, performance, the Variant API, and the typed interface API.
- Boss-installable package for Delphi 12, zero third-party runtime dependencies (builds on
System.JSON).
Installation
Boss is the easiest path:
# Pin to a release tag (recommended)
boss install github.com/gabrielcb08/json4d@v0.1.0-alpha.1
# Or the latest commit (not pinned — use only for experimentation)
boss install github.com/gabrielcb08/json4dAfter installation, import Json4D from your unit — the facade re-exports every public type and attribute, so this single uses is all you need:
uses
Json4D;See docs/BOSS.md for the full Boss workflow (publishing tags, updating
boss.json, releasing).
Quick Start
uses
Json4D;
var
Json : TJsonValue;
Name : string;
Phone : Integer;
begin
Json := TJson.Parse('{"name":"Gabriel","contacts":[{"phone":99999}]}');
Name := Json['name']; // implicit conversion to string
Phone := Json['contacts'][0]['phone']; // implicit conversion to Integer
Writeln(Name, ' / ', Phone);
end;See samples/BasicUsage/BasicUsage.dpr for a runnable end-to-end demo of every public feature.
The Four APIs
Json4D exposes the same parsed document through four ergonomic surfaces. Pick the one that fits the call site — they interoperate freely.
| API | Entry point | Best for |
|---|---|---|
| Classic / fluent | TJson.Parse → TJsonValue |
General read/write, builders, hot paths |
| Dynamic Variant | TJson.ParseVariant → Variant |
Quick scripts, JS-style obj.field access |
| Typed interface | TJson.Deserialize<T> → T |
DTOs, IntelliSense, compile-time shape |
| Design-time component | TJson4D on the ORData palette |
RAD forms, no-code JSON wiring |
flowchart LR
S["JSON string"] -->|TJson.Parse| C["TJsonValue<br/>(fluent)"]
S -->|TJson.ParseVariant| V["Variant<br/>(dynamic dot access)"]
S -->|TJson.Deserialize<T>| T["T proxy<br/>(typed)"]
C -.same IJsonDocument.-> V
V -.same IJsonDocument.-> TReading Data
Bracket Access
Json := TJson.Parse('{"client":{"name":"Maria","age":30,"tags":["vip","gold"]}}');
ShowMessage(Json['client']['name']); // "Maria"
ShowMessage(Json['client']['age']); // 30 (implicit string conversion)
ShowMessage(Json['client']['tags'][0]); // "vip"Chained access never raises on missing intermediates — Json['a']['b'][10]['c'].AsString('fallback') returns 'fallback' even when none of those exist.
Safe Reads with Defaults
Every primitive accessor takes an optional default applied when the path is missing or null:
City := Json['client']['address']['city'].AsString('Not informed');
Age := Json['client']['age'].AsInteger(18);
Active := Json['client']['active'].AsBoolean(False);
Salary := Json['client']['salary'].AsFloat(0.0);Path and Compiled Path
For programmatic lookups or hot-path reads:
Phone := Json.Path('contacts[0].phone'); // dynamic, single hash lookup
var CP := Json.CompilePath('contacts[0].phone'); // pre-computes text + hash
Phone := Json.Get(CP); // repeated reads via cached pathThe compiled path is the same shape as Path's argument; the difference is amortization on repeated lookups.
Writing Data
Bracket Setter
Missing parent objects are created on the fly:
Json['name'] := 'João';
Json['client']['address']['city'] := 'São Paulo'; // builds client, address⚠️ The bracket setter refuses paths containing array indices —
Json['items'][0]['x'] := ...raisesEJson4DPathException. Arrays must be built with the fluent API so positional semantics stay explicit.
Fluent Builder
Json := TJson.ObjectValue;
Json
.Add('name', 'João')
.Add('age', 30)
.AddObj('address')
.Add('city', 'São Paulo')
.Add('state', 'SP')
.EndObj
.AddArray('phones')
.Add('1111')
.Add('2222')
.EndArray;
Writeln(Json.ToPrettyJson);Object and array contexts have separate overloads — Add(key, value) for objects, Add(value) for arrays. EndObj / EndArray walk back to the parent; at root they return Self (idempotent).
Dynamic Variant API
TJson.ParseVariant returns a Variant with JavaScript/PHP-style dynamic access — dot for keys, brackets for array indices, both for reading and writing. JSON keys are case-sensitive (JsonVar.name ≠ JsonVar.Name).
uses
Json4D;
var
Doc : Variant;
begin
Doc := TJson.ParseVariant('{"user":{"name":"Gabriel","roles":["admin","ops"]}}');
// Read — an explicit cast is required (the result is a Variant, not a string)
Writeln(string(Doc.user.name)); // "Gabriel"
Writeln(string(Doc.user.roles[0])); // "admin"
// Write — overwrites an existing key
Doc.user.name := 'João';
// Write — chained access auto-creates intermediate objects
Doc.user.contact.phone := '18981367440';
// Serialize back
Writeln(string(Doc.ToJson));
end;Reads return a Variant, so wrap them in an explicit string(...), Integer(...), Boolean(...) cast at the call site. Use this API for quick, dynamic access; reach for the typed interface API when you want IntelliSense and compile-time checking.
Typed Interface API
Declare an interface, get IntelliSense, and parse JSON straight into it. The library uses System.Rtti.TVirtualInterface to materialize a dynamic proxy that answers each getter/setter by routing through the parsed IJsonDocument.
uses
Json4D;
type
IAddress = interface(IInvokable)
['{B1E11AC0-7F46-4D27-8E2A-3C5C4A91D001}']
function city : string;
function state : string;
end;
IClient = interface(IInvokable)
['{B1E11AC0-7F46-4D27-8E2A-3C5C4A91D003}']
function name : string;
function age : Integer;
function active : Boolean;
function salary : Double;
[JsonName('first_name')]
function firstName : string;
[JsonRequired]
function cpf : string;
[JsonIgnore]
function internalPassword : string;
[JsonDefault('99')]
function score : Integer;
function address : IAddress;
function tags : TArray<string>;
procedure SetName(const V : string);
end;
var
Client : IClient;
Output : string;
begin
Client := TJson.Deserialize<IClient>(
'{"name":"Maria","age":28,"first_name":"Maria","cpf":"123",' +
'"address":{"city":"Curitiba","state":"PR"},"tags":["vip","gold"]}'
);
Writeln(Client.name); // "Maria"
Writeln(Client.firstName); // "Maria" (read from "first_name")
Writeln(Client.score.ToString); // 99 (default — key absent)
Writeln(Client.address.city); // "Curitiba"
Client.SetName('Maria Joana');
Output := TJson.Serialize<IClient>(Client); // round-trip preserves extras
end;Attributes
All attributes live in Json4D.Presentation.Serialization.Attributes and are re-exported by the Json4D facade — uses Json4D; is enough.
| Attribute | Effect |
|---|---|
[JsonName('key')] |
Maps the method to a JSON key different from the Delphi identifier. |
[JsonIgnore] |
Skipped by Serialize; returns the type default on Deserialize. |
[JsonRequired] |
Raises EJson4DRequiredFieldException when the key is absent in the JSON. |
[JsonFormat('fmt')] |
Custom formatting for TDateTime/floats — applied via FormatDateTime/FormatFloat and parsed inversely. |
[JsonDefault('val')] |
Value to use when the key is missing (overrides the type default 0/''/False). |
Rules and Limitations
Tmust have a GUID and inherit fromIInvokable(or have{$M+}enabled).- Setters follow the
procedure SetXxx(const V: ...)convention — theSetprefix is stripped and the first letter lowercased (SetName→name). ForPascalCaseorsnake_caseJSON, annotate with[JsonName(...)]. - Setters accept primitive types only. To replace whole subtrees, use the classic
TJson.Parse+ bracket setter. TArray<T>is supported for primitiveTand interfaceT.IList<T>/IEnumerable<T>are on the roadmap.Serialize<T>on proxies (instances returned byDeserialize) performs an exact round-trip — keys not declared onTare preserved.Serialize<T>on arbitrary implementations (mocks, your own classes) walks RTTI: only getters declared inTend up in the output (after[JsonIgnore]filtering).- Json4D ignores unknown attributes (e.g. a consumer-defined
[PrimaryKey]). The library only deals with JSON — ORM/identity concerns belong to the consumer project.
The TJson4D Component
For RAD / form-based workflows, drop a TJson4D non-visual component (palette ORData) onto a form and drive it from the Object Inspector and code, without importing the units explicitly. It wraps the same engine as TJson, so everything above applies. See samples/FormUsage for a VCL example.
Comparison with System.JSON
Same task on both — read a value, mutate it, serialize back:
// System.JSON (RTL)
var Obj := TJSONObject.ParseJSONValue(S) as TJSONObject;
try
if (Obj <> nil) and (Obj.GetValue<TJSONObject>('client') <> nil) then
Writeln(Obj.GetValue<TJSONObject>('client').GetValue<string>('name'));
(Obj.GetValue<TJSONObject>('client') as TJSONObject).RemovePair('name').Free;
(Obj.GetValue<TJSONObject>('client') as TJSONObject).AddPair('name', 'João');
Writeln(Obj.ToJSON);
finally
Obj.Free;
end;// Json4D
var Json := TJson.Parse(S);
Writeln(Json['client']['name']); // null-safe, no casts
Json['client']['name'] := 'João'; // bracket setter
Writeln(Json.ToJson); // no manual cleanup| Concern | System.JSON |
Json4D |
|---|---|---|
| Memory management | Manual try / finally / Free |
Interface-based — automatic |
| Missing key on deep path | Raises / returns nil |
Returns null-safe wrapper with default |
| Lookup cost on repeated reads | O(depth) tree walks | O(1) average — indexed at parse |
| Mutation | RemovePair + AddPair (mutates pair) |
Bracket setter, fluent builder, or typed setter |
| Type-safe binding | Manual casts / hand-written DTOs | TJson.Deserialize<T> via interface proxy |
Json4D is not a replacement for System.JSON — it builds on top of it (the parser delegates to System.JSON). The win is in the consumer ergonomics and the indexing layer above the parsed tree.
Performance
Benchmarks live in tests/Json4D.Tests/Performance/Json4D.Tests.Performance.BenchmarkTest.pas and run as part of the test suite.
Reference numbers from a Debug build on a typical developer laptop:
| Operation | Sample size | Elapsed |
|---|---|---|
TJson.Parse (builds tree + path index) |
10 000 objects | ~109 ms |
Dynamic path lookup (Json.Path('...')) |
10 000 iterations | ~2 ms |
Compiled path (Json.Get(CP)) |
10 000 iterations | ~2 ms |
Parse is heavier by design (it indexes every node up front). Reads are then dictionary-backed and avoid recursive scans. See docs/PERFORMANCE.md for the strategy and expected behavior.
Architecture
Json4D follows Clean Architecture with four layers and dependencies pointing inward — outer layers depend only on Contracts, never on concrete implementations.
src/
?????? 01 - Core/ ??? pure types, exceptions, path helpers (no dependencies)
?????? 02 - Contracts/ ??? every interface/port (IJsonValue, IJsonDocument, ???)
?????? 03 - Infrastructure/ ??? System.JSON adapter, indexing, compiled paths
??? ?????? Indexing/
??? ?????? SystemJson/
?????? 04 - Presentation/ ??? TJson facade, TJsonValue wrapper, proxies, Variant type, component
?????? Facades/ Wrappers/ Proxies/ Serialization/ Variants/ Components/flowchart TD
P["04 · Presentation<br/>TJson · TJsonValue · proxies · Variant · TJson4D"]
I["03 · Infrastructure<br/>System.JSON adapter · indexing · compiled paths"]
K["02 · Contracts<br/>IJsonValue · IJsonDocument · IJsonParser · …"]
D["01 · Core<br/>TJsonValueKind · TJsonPath · exceptions"]
P --> K
I --> K
K --> D
P -. wires at runtime .-> IThe facade (TJson) wires the concrete Infrastructure directly — there is no IoC container and no global state. Full layer breakdown, component responsibilities, and parse/read/mutation/typed-proxy flows (with sequence and class diagrams) are in docs/ARCHITECTURE.md.
Roadmap
Tracked publicly so contributors know where help is welcome:
- [ ]
IList<T>/IEnumerable<T>support in the typed interface API — currently onlyTArray<T>is wired. - [ ] Interface/array setters — write whole subtrees via the typed API (currently primitives only).
- [ ] JSON Schema validation — optional companion package.
- [ ] Streaming parser for very large documents (gigabyte-scale).
- [ ] Source-generator tooling to emit concrete classes from interfaces (faster than
TVirtualInterfacefor hot paths). - [ ] Win64 + macOS / Linux verified builds (CI matrix).
Open an issue if you want to tackle any of these — happy to discuss design before code.
Documentation
| Document | What's inside |
|---|---|
| docs/ARCHITECTURE.md | Layering, components, parse/read/mutation/typed flows, diagrams |
| docs/TESTING.md | Test framework, fixture layout, how to run |
| docs/PERFORMANCE.md | Indexing strategy, benchmark behavior |
| docs/CONTRIBUTING.md | Coding standards, PR checklist |
| docs/BOSS.md | Boss install, release workflow |
| docs/CHANGELOG.md | Version-by-version change log |
Contributing
Contributions are welcome — bug reports, doc fixes, new features. Before sending a PR:
- Read docs/CONTRIBUTING.md.
- Add tests for behavioral changes (DUnitX).
- Preserve layer boundaries (no
Presentation↔Infrastructureshortcuts that skipContracts). - Update docs/CHANGELOG.md.
Run the suite locally:
# Build the package
msbuild packages/Delphi12/Json4D.dproj /p:Config=Debug /p:Platform=Win32
# Build and run tests
msbuild tests/Json4D.Tests/Json4D.Tests.dproj /p:Config=Debug /p:Platform=Win32
tests/Json4D.Tests/build/Win32/Debug/Json4D.Tests.exe --consoleSet Json4D_Spring4DPath if Spring4D is not on the global search path (used by some test fixtures):
$env:Json4D_Spring4DPath = "path/to/spring4d/Source"License
Released under the MIT License — free for commercial and personal use.

