JSON Serialization¶
Serialize Trysil entities and lists to JSON strings or Delphi JSON objects.
Entity to JSON String¶
var LConfig := TTJSonSerializerConfig.Create(-1, False);
var LJson := LContext.EntityToJSon<TPerson>(LPerson, LConfig);
// Returns: {"ID":1,"Firstname":"David","Lastname":"Lastrucci","VersionID":1}
Entity to TJSonObject¶
When you need to manipulate the JSON object before converting to a string:
var LJsonObj := LContext.EntityToJSonObject<TPerson>(LPerson, LConfig);
try
// Work with TJSonObject directly
LJsonObj.AddPair('ComputedField', 'value');
LResult := LJsonObj.ToJSON;
finally
LJsonObj.Free;
end;
List to JSON String¶
var LJson := LContext.ListToJSon<TPerson>(LPersons, LConfig);
// Returns: [{"ID":1,...},{"ID":2,...}]
List to TJSonArray¶
var LArray := LContext.ListToJSonArray<TPerson>(LPersons, LConfig);
try
// Work with TJSonArray directly
finally
LArray.Free;
end;
Dataset to JSON¶
Convert any TDataset to a JSON string, regardless of whether it comes from Trysil:
The field names come out lower cased, which is not the camelCase an entity
produces. The difference is deliberate: an entity takes its names from the
Delphi members, while here they arrive from the database, where the case
depends on the dialect - CUSTOMERNAME on Oracle, customername on
PostgreSQL. There is no camelCase to recover from either, so lower case is the
one form that is the same everywhere. Alias the columns in the SELECT if you
want to decide the names yourself.
Metadata to JSON¶
Export entity column metadata (names, types, sizes) as JSON:
This is useful for building dynamic UIs or generating documentation from entity definitions.
The payload names what it describes with entity, the class name without the T that Delphi puts in front of a type: TAPIOrder is reported as APIOrder. Only that one letter is dropped, and only when a capital follows it, so an acronym keeps its shape and a class called Temp is left alone. It used to be tableName, the name of the table in the database, and that was the one line in the payload that spoke of the schema: every other name in it - the columns, the primary key, the version column - is already the JSON name the client sees in ordinary responses. The table name is of no use to a client that addresses /api/orders, it changes when you rename a table and would break that client for a change that does not concern it, and it is the first thing worth having for anyone probing the API for a way into the database underneath. The class name is stable across a schema refactor and identifies the type, which is what a client caching metadata needs.
The array is called properties, not columns, because that is what it holds: one entry per mapped member, reported with the JSON name the client sees in ordinary responses. A [TColumn('CustomerID')] on a TTLazy<TCustomer> appears as customerID, not as the column behind it. Reading it as a list of database columns was the mistake the old name invited.
Each property reports name, type, and - when they are not zero - size and precision. For a decimal column the two mean what they mean in the database and not what their names suggest: size is the decimal scale and precision the total number of digits, so decimal(19,4) comes back as "size": 4, "precision": 19. A client that validates an amount needs both: size alone does not say how many integer digits fit. precision is new in 2.0.0 and, like size, is omitted when zero, so an existing payload only gains a key.
A column carrying TNotFilterable also reports "filterable": false. The pair appears only when the column cannot be filtered, so absence means filterable. From 2.0.0 that covers one case more: a column no response returns is not filterable either, so a member carrying [TJSonIgnoreSerialize] reports filterable: false beside its readable: false. The one exception is a column map built by hand with no member, which the mapper never produces: it has no JSON name and stays filterable by its column name. A client that builds a filter UI from the metadata should hide those columns, otherwise the first where on one comes back as a 400.
The direction is declared the same way. properties holds one entry per member the entity serializes or deserializes, and only the exception is written: "readable": false on a member the body may write and no response returns - a password taken on create is the case from the manual - and "writable": false on one that is returned and never read from the body, which includes every change tracking column, because the deserializer refuses those. A member that goes both ways gains no key. Only [TJSonIgnore], which is neither direction, is left out of the payload entirely, and the primaryKey and versionColumn pairs at the top answer the same question as the array below them: a key the body may write and no response returns is still named as the key, with "readable": false on its entry.
Ignoring Fields¶
Use the TJSonIgnore attribute to exclude specific fields from serialization:
type
[TTable('Persons')]
[TSequence('PersonsID')]
TPerson = class
strict private
[TPrimaryKey]
[TColumn('ID')]
FID: TTPrimaryKey;
[TColumn('Firstname')]
FFirstname: String;
[TColumn('Lastname')]
FLastname: String;
[TJSonIgnore]
[TColumn('InternalField')]
FInternalField: String;
[TVersionColumn]
[TColumn('VersionID')]
FVersionID: TTVersion;
public
property ID: TTPrimaryKey read FID;
property Firstname: String read FFirstname write FFirstname;
property Lastname: String read FLastname write FLastname;
property InternalField: String read FInternalField write FInternalField;
property VersionID: TTVersion read FVersionID;
end;
When serialized, InternalField will not appear in the JSON output.
Serialization Config¶
The TTJSonSerializerConfig record controls serialization depth and detail inclusion. See JSON Configuration for details.
An Int64 is emitted as a JSON number
Which is exact on the wire and lossy in JavaScript: Number holds whole
values up to 2^53 - 9,007,199,254,740,992 - and silently rounds anything
larger. A BIGINT primary key generated from a timestamp, or a quantity
in thousandths, can reach that range.
Trysil emits every number as a JSON number, Currency included, so no
mapping quotes it for you. If the values in a column go beyond that
range, map the member as String rather than relying on the client to
read it back intact.
In the other direction both forms are accepted: a client that holds a
large whole number as a string may send it quoted, and the value reaches
the Int64 member without passing through a Double.
Cycles¶
Two entities that point at each other - an employee and a manager, a category
and its parent - form a cycle. With MaxLevels at -1 the depth gate is
always open, so the serializer follows the reference each way in turn.
It stops on its own: while it descends it remembers which entities it is
inside, by class and primary key, and when one of them comes round again it
emits only the reference id, exactly as MaxLevels = 0 does. The JSON is
finite and valid, and no exception is raised.
The identity check is on the key rather than on the instance because a JSON
context refuses the identity map, so each step loads a fresh object: two
instances of the same row are the same entity as far as a cycle is concerned.
An entity with no [TPrimaryKey] - a DTO filled by RawSelect - has no
identity to compare, and is serialized as it was before.