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.

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)

  1. Clone or download delphi-neon:
    git clone https://github.com/paolo-rossi/delphi-neon.git
    
  2. Clone or download horse-neon:
    git clone https://github.com/usofm/horse-neon.git
    
  3. Add the Source directory of Neon and src directory of horse-neon to your Delphi project's Search Path (Project -> Options -> Building -> Delphi Compiler -> Search Path):
    • C:\path\to\delphi-neon\Source
    • C:\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 TJSONValue and stores it in Req.Body(LJSON). This means you can still access it using standard Req.Body<TJSONValue> / Req.Body<TJSONObject>.
  • Response: Serializes Res.Content to a JSON string in RawWebResponse.Content after executing the request pipeline.
  • This drop-in design ensures that migrating from jhonson is a quick, one-line change in your uses clause 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 via TNeon.ObjectToJSONString(Res.Content, Config) using full RTTI mapping.
  • This is the key advantage over jhonson: you can simply call Res.Send<TUser>(LUser) or send list objects like TObjectList<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 a TJSONObject first.

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 BodyAsRecord and SendRecord (instead of overloading BodyAs and Send with 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

ParameterOptions / Description
SetMembersControls which class members are serialized:
TNeonMembers.Fields: Serialize/deserialize backing fields directly.
TNeonMembers.Properties: Consider only properties.
SetMemberCaseControls 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.
SetVisibilityFilter members by their access visibility:
mvPrivate, mvProtected, mvPublic, mvPublished
SetUseUTCDateWhen True, treats Delphi TDateTime values as UTC and serializes them in standard ISO 8601 UTC format.
SetPrettyPrintWhen 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 THorseInstance running on different ports can use different Neon configurations (e.g. one using CamelCase and another using SnakeCase or different custom serialization rules) without bleeding into each other or causing concurrency issues.

💡 Best Practices

  1. 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.
  2. Naming Consistency: Choose a single TNeonCase formatting standard (e.g., TNeonCase.CamelCase for typical REST services) and use it consistently across all your endpoints.
  3. 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.
  4. 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.Neon is Delphi-only and does not support Lazarus/FPC. For Lazarus projects, continue using Horse.Jhonson.

📄 License

Horse.Neon is licensed under the MIT License.