Horse.Neon 🦄⚡
August 3, 2026 · View on GitHub
Neon (RTTI-based JSON serialization engine) middleware for Horse.
Horse.Neon allows you to seamlessly serialize and deserialize Delphi objects (TObject descendants, classes, records, generic lists, etc.) to and from JSON using the powerful Neon serialization library.
It serves as a drop-in, feature-rich replacement for Horse.Jhonson.
⚙️ Prerequisites & Neon Installation
Before using Horse.Neon, you must have Neon (delphi-neon) installed in your Delphi environment.
Option 1: Using Boss Package Manager (Recommended)
If you use Boss, install delphi-neon and horse-neon in your project:
boss install github.com/paolo-rossi/delphi-neon
boss install github.com/usofm/horse-neon
Option 2: Manual Installation (Library Search Path)
- Clone or download delphi-neon:
git clone https://github.com/paolo-rossi/delphi-neon.git - Clone or download horse-neon:
git clone https://github.com/usofm/horse-neon.git - Add the
Sourcedirectory of Neon andsrcdirectory ofhorse-neonto your Delphi project's Search Path (Project -> Options -> Building -> Delphi Compiler -> Search Path):C:\path\to\delphi-neon\SourceC:\path\to\horse-neon\src
⚠️ Note: Neon relies on Delphi RTTI (
System.Rtti) and is supported on Delphi XE7 and higher (including Delphi 10.x, 11 Alexandria, and 12 Athens). Neon does not support FPC/Lazarus.
⚡ Quick Start
Migration from Horse.Jhonson
Migrating from Horse.Jhonson to Horse.Neon is as simple as updating your uses and middleware initialization:
uses
- Horse, Horse.Jhonson;
+ Horse, Horse.Neon;
begin
- THorse.Use(Jhonson);
+ THorse.Use(Neon);
Complete Usage Example
program MyHorseApp;
{$APPTYPE CONSOLE}
uses
Horse,
Horse.Neon,
Neon.Core.Persistence,
Neon.Core.Types,
System.SysUtils;
type
TUser = class
private
FId: Integer;
FName: string;
FEmail: string;
published
property Id: Integer read FId write FId;
property Name: string read FName write FName;
property Email: string read FEmail write FEmail;
end;
begin
// Register Horse.Neon middleware with default configuration
THorse.Use(Neon);
// POST: Receive JSON body and deserialize directly to TUser class
THorse.Post('/users',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TNextProc)
var
LUser: TUser;
begin
LUser := Req.BodyAs<TUser>; // Neon deserializes JSON -> TUser
try
LUser.Id := 42;
Res.Send<TUser>(LUser).Status(THTTPStatus.Created);
LUser := nil; // Transfer ownership to Horse to prevent double-free
finally
LUser.Free; // Safely frees LUser if an exception occurs before Res.Send
end;
end);
// GET: Return TUser instance automatically serialized to JSON
THorse.Get('/users/:id',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TNextProc)
var
LUser: TUser;
begin
LUser := TUser.Create;
LUser.Id := Req.Params.Field('id').AsInteger;
LUser.Name := 'Hamyar';
LUser.Email := 'hamyar@example.com';
Res.Send<TUser>(LUser); // Auto-serialized by Neon RTTI
end);
THorse.Listen(9000);
end.
💡 Key Features & Architectural Highlights
1. Same Contract as Horse.Jhonson
The middleware preserves the exact pattern used in Horse.Jhonson:
- Request: Automatically parses the incoming JSON body into a
TJSONValueand stores it inReq.Body(LJSON). This means you can still access it using standardReq.Body<TJSONValue>/Req.Body<TJSONObject>. - Response: Serializes
Res.Contentto a JSON string inRawWebResponse.Contentafter executing the request pipeline. - This drop-in design ensures that migrating from
jhonsonis a quick, one-line change in yourusesclause without modifying your existing handler routing.
2. Dual Serialization Paths (Inspired by WiRL)
Following the pattern found in WiRL's TWiRLObjectProvider:
TJSONValue& descendants: Serialized directly via.ToJSON(zero RTTI overhead for raw JSON objects).- Any other Delphi
TObject: Serialized viaTNeon.ObjectToJSONString(Res.Content, Config)using full RTTI mapping. - This is the key advantage over
jhonson: you can simply callRes.Send<TUser>(LUser)or send list objects likeTObjectList<TUser>directly, and the middleware automatically handles RTTI-based serialization to a clean JSON payload without you having to manually write conversion methods or map values to aTJSONObjectfirst.
3. Typed Deserialization & Record Support (BodyAsRecord / SendRecord)
In Delphi, record types are value types and cannot be restricted by class constraints (such as T: class) or assigned to TObject variables. To bypass this language constraint, Horse.Neon provides dedicated record helper methods to serialize and deserialize record types:
Deserializing Records with BodyAsRecord<T>
var
LContact: TContact;
begin
LContact := Req.BodyAsRecord<TContact>; // Deserializes request JSON body directly to record
// No need to free LContact because records are value types!
end;
Serializing and Sending Records with SendRecord<T>
var
LContact: TContact;
begin
LContact.Phone := '+1-555-0199';
LContact.Address := '123 Main St';
Res.SendRecord<TContact>(LContact).Status(THTTPStatus.Created); // Sends record as JSON
end;
💡 Design Note: These methods are named
BodyAsRecordandSendRecord(instead of overloadingBodyAsandSendwith constraint variations) to avoid compiler ambiguity and prevent class helper method shadowing in Delphi.
4. Built-in Dataset Support (TDataSet, TClientDataSet, TFDMemTable)
Horse.Neon automatically registers TDataSetSerializer from Neon.Core.Serializers.DB into TNeonConfiguration.Default upon initialization.
This allows you to return any TDataSet descendant (such as TClientDataSet, TFDMemTable, or TFDQuery) or an array of datasets (TArray<TDataSet>) directly in your Horse endpoints, serializing dataset records into clean JSON arrays without any generic RTTI recursion or stack overflow issues:
// GET: Return TDataSet (e.g. TClientDataSet) as JSON array
THorse.Get('/dataset',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TNextProc)
var
LDataSet: TDataSet;
begin
LDataSet := DataSet1;
Res.Send<TDataSet>(LDataSet); // Horse owns LDataSet and will serialize & free it!
end);
// GET: Return TFDMemTable as JSON array
THorse.Get('/fddataset',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TNextProc)
var
LFDMT: TFDMemTable;
begin
LFDMT := DataSet2;
Res.Send<TFDMemTable>(LFDMT);
end);
// GET: Return TArray<TDataSet> (or any TArray<T>) as JSON array using SendArray helper
THorse.Get('/datasets',
procedure(Req: THorseRequest; Res: THorseResponse; Next: TNextProc)
var
LDataSets: TArray<TDataSet>;
I: Integer;
begin
LDataSets := DataSets;
try
Res.SendArray<TDataSet>(LDataSets);
finally
for I := 0 to High(LDataSets) do
LDataSets[I].Free;
end;
end);
5. Flexible Neon Configuration (INeonConfiguration)
You can pass custom Neon configuration rules to the middleware. Horse.Neon allows you to fine-tune Neon's serialization behavior using the standard Neon configuration parameters:
uses Horse, Horse.Neon, Neon.Core.Persistence, Neon.Core.Types;
begin
THorse.Use(Neon(
TNeonConfiguration.Default
.SetMemberCase(TNeonCase.CamelCase) // Format JSON keys as camelCase
.SetVisibility([mvPublic, mvPublished]) // Only serialize public and published members
.SetIgnoreFieldPrefix(True) // Ignore the 'F' prefix on backing fields
.SetUseUTCDate(True) // Output ISO 8601 UTC dates
.SetPrettyPrint(False) // Compact JSON output
));
Key Configurable Parameters
| Parameter | Options / Description |
|---|---|
SetMembers | Controls which class members are serialized: • TNeonMembers.Fields: Serialize/deserialize backing fields directly.• TNeonMembers.Properties: Consider only properties. |
SetMemberCase | Controls JSON property key case formatting: • TNeonCase.Unchanged: Leaves names as-is (e.g., UserName).• TNeonCase.CamelCase: camelCase (e.g., userName).• TNeonCase.SnakeCase: snake_case (e.g., user_name).• TNeonCase.PascalCase: PascalCase (e.g., UserName).• TNeonCase.KebabCase: kebab-case (e.g., user-name).• TNeonCase.LowerCase: lowercase (e.g., username).• TNeonCase.UpperCase: UPPERCASE (e.g., USERNAME).• TNeonCase.ScreamingSnake: SCREAMING_SNAKE_CASE (e.g., USER_NAME).• TNeonCase.Custom: Uses a custom case converter function. |
SetVisibility | Filter members by their access visibility: • mvPrivate, mvProtected, mvPublic, mvPublished |
SetUseUTCDate | When True, treats Delphi TDateTime values as UTC and serializes them in standard ISO 8601 UTC format. |
SetPrettyPrint | When True, formats the output JSON string with line breaks and indentations (great for development, but increases payload size). |
6. Thread-Safety & Instance Isolation
Inspired by WiRL's configuration architecture, the Neon configuration interface (INeonConfiguration) is captured inside the middleware closure during initialization:
- Because the configuration interface is read-only after creation, the configuration state is completely thread-safe.
- Multiple instances of
THorseInstancerunning on different ports can use different Neon configurations (e.g. one usingCamelCaseand another usingSnakeCaseor different custom serialization rules) without bleeding into each other or causing concurrency issues.
💡 Best Practices
- Visibility Control: For secure APIs, it's usually best to limit visibility to
[mvPublic, mvPublished]so internal private implementation fields aren't accidentally leaked into your JSON payloads. - Naming Consistency: Choose a single
TNeonCaseformatting standard (e.g.,TNeonCase.CamelCasefor typical REST services) and use it consistently across all your endpoints. - Date Consistency: Turn on
SetUseUTCDate(True)to serialize date/time fields in standard UTC ISO 8601 strings, which is highly recommended for client/server interoperability. - PrettyPrint in Production: Avoid leaving
SetPrettyPrint(True)on in production environments to minimize network bandwidth consumption.
🛠️ Verification & Building
- Manual Compilation: To verify compilation, ensure that the Neon library source folder is present in your compiler search path.
- Lazarus/FPC Support: Because the Neon serialization library depends heavily on advanced Delphi RTTI features (such as properties, lists, and record attributes),
Horse.Neonis Delphi-only and does not support Lazarus/FPC. For Lazarus projects, continue usingHorse.Jhonson.
📄 License
Horse.Neon is licensed under the MIT License.