Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works

Random project

json4d

View on GitHub

Anyone may press Interesting, signed in or not. It helps others find this project.

Member rating

No ratings yet.

Sign in to rate 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.

English · Português (BR)


Table of Contents


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:

  1. Reads are fast. Every parse builds an in-memory path index, so subsequent lookups are O(1) dictionary reads — no recursive tree scans.
  2. Reads are safe. Missing keys return null-safe wrappers with defaults; deep Json['a']['b']['c'] chains never raise on absent intermediates.
  3. Reads can be typed. Declare an interface, get IntelliSense, and let TJson.Deserialize<IClient>(json) return something that behaves exactly like IClient — including nested interfaces and TArray<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 TJsonValue API 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 / EndArray for object and array contexts.
  • Dynamic Variant API — TJson.ParseVariant for JavaScript-style JsonVar.user.name dot access (read and write).
  • Typed interface API — TJson.Deserialize<T> / TJson.Serialize<T> backed by TVirtualInterface proxies.
  • RTTI custom attributes — [JsonName], [JsonIgnore], [JsonRequired], [JsonFormat], [JsonDefault].
  • TJson4D non-visual component for the ORData palette — 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:

bash
# 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/json4d

After installation, import Json4D from your unit — the facade re-exports every public type and attribute, so this single uses is all you need:

delphi
uses
  Json4D;

See docs/BOSS.md for the full Boss workflow (publishing tags, updating boss.json, releasing).

Quick Start

delphi
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
mermaid
flowchart LR
    S["JSON string"] -->|TJson.Parse| C["TJsonValue<br/>(fluent)"]
    S -->|TJson.ParseVariant| V["Variant<br/>(dynamic dot access)"]
    S -->|TJson.Deserialize&lt;T&gt;| T["T proxy<br/>(typed)"]
    C -.same IJsonDocument.-> V
    V -.same IJsonDocument.-> T

Reading Data

Bracket Access

delphi
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:

delphi
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:

delphi
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 path

The 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:

delphi
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'] := ... raises EJson4DPathException. Arrays must be built with the fluent API so positional semantics stay explicit.

Fluent Builder

delphi
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).

delphi
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.

delphi
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

  • T must have a GUID and inherit from IInvokable (or have {$M+} enabled).
  • Setters follow the procedure SetXxx(const V: ...) convention — the Set prefix is stripped and the first letter lowercased (SetName → name). For PascalCase or snake_case JSON, annotate with [JsonName(...)].
  • Setters accept primitive types only. To replace whole subtrees, use the classic TJson.Parse + bracket setter.
  • TArray<T> is supported for primitive T and interface T. IList<T> / IEnumerable<T> are on the roadmap.
  • Serialize<T> on proxies (instances returned by Deserialize) performs an exact round-trip — keys not declared on T are preserved.
  • Serialize<T> on arbitrary implementations (mocks, your own classes) walks RTTI: only getters declared in T end 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:

delphi
// 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;
delphi
// 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.

pascal
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/
mermaid
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 .-> I

The 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 only TArray<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 TVirtualInterface for 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:

  1. Read docs/CONTRIBUTING.md.
  2. Add tests for behavioral changes (DUnitX).
  3. Preserve layer boundaries (no Presentation ↔ Infrastructure shortcuts that skip Contracts).
  4. Update docs/CHANGELOG.md.

Run the suite locally:

bash
# 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 --console

Set Json4D_Spring4DPath if Spring4D is not on the global search path (used by some test fixtures):

powershell
$env:Json4D_Spring4DPath = "path/to/spring4d/Source"

License

Released under the MIT License — free for commercial and personal use.