Tina4

Chapter 3: JSON & Data Binding#

The Bridge Between APIs and Grids#

Your API returns JSON. Your grid displays MemTable rows. Between these two worlds sits a translation layer -- field names need converting, dates need formatting, nested objects need flattening, and records need matching for updates. Tina4 Delphi handles all of this with a set of utility functions and one component.

This chapter covers the full JSON pipeline: parsing raw strings, converting database queries to JSON, populating MemTables from JSON, syncing changes, and binding data declaratively with TTina4JSONAdapter.


1. JSON Parsing Utilities#

Before you can work with JSON data, you need to parse it. Tina4Core.pas provides four parsing functions that handle the common cases.

StrToJSONObject#

Parses a JSON string into a TJSONObject. Returns nil if parsing fails -- always check with Assigned.

pascal
uses Tina4Core;โ€‹var Obj := StrToJSONObject('{"name": "Alice", "age": 30, "active": true}');try  if Assigned(Obj) then  begin    ShowMessage(Obj.GetValue<String>('name'));     // 'Alice'    ShowMessage(Obj.GetValue<Integer>('age').ToString);  // '30'    ShowMessage(Obj.GetValue<Boolean>('active').ToString); // 'True'  end  else    ShowMessage('Invalid JSON');finally  Obj.Free;end;

StrToJSONArray#

Parses a JSON string into a TJSONArray. Use this when the root element is an array:

pascal
var Arr := StrToJSONArray('[{"id": 1}, {"id": 2}, {"id": 3}]');try  if Assigned(Arr) then    for var I := 0 to Arr.Count - 1 do      ShowMessage((Arr.Items[I] as TJSONObject).GetValue<String>('id'));finally  Arr.Free;end;

StrToJSONValue#

When you do not know whether the input is an object, array, string, number, or boolean:

pascal
var Val := StrToJSONValue(SomeInput);try  if Val is TJSONObject then    ProcessObject(Val as TJSONObject)  else if Val is TJSONArray then    ProcessArray(Val as TJSONArray)  else    ShowMessage('Primitive: ' + Val.Value);finally  Val.Free;end;

BytesToJSONObject#

Parses raw TBytes directly -- the typical output of SendHttpRequest:

pascal
var  StatusCode: Integer;  Response: TBytes;begin  Response := SendHttpRequest(StatusCode, 'https://api.example.com', '/users');  var JSON := BytesToJSONObject(Response);  try    if Assigned(JSON) then      Memo1.Lines.Text := JSON.Format;  finally    JSON.Free;  end;end;

GetJSONFieldName#

Strips surrounding quotes from a JSON field name. Useful when iterating TJSONPair elements:

pascal
GetJSONFieldName('"firstName"');  // 'firstName'GetJSONFieldName('age');          // 'age'

2. TTina4JSONAdapter -- Static JSON to MemTable#

TTina4JSONAdapter is the declarative way to bind JSON data to a TFDMemTable. Drop it on your form, set the JSON, set the data key, and execute. No parsing code. No field definition code. No population loops.

From Static JSON#

pascal
// Design-time or runtime:Tina4JSONAdapter1.MemTable := FDMemTable1;Tina4JSONAdapter1.DataKey := 'products';Tina4JSONAdapter1.JSONData.Text :=  '{"products": [' +  '  {"id": "1", "name": "Widget", "price": 9.99},' +  '  {"id": "2", "name": "Gadget", "price": 24.99},' +  '  {"id": "3", "name": "Doohickey", "price": 4.50}' +  ']}';Tina4JSONAdapter1.Execute;// FDMemTable1 now has 3 rows with id, name, price columns

From MasterSource#

Link the adapter to a TTina4RESTRequest and it auto-executes whenever the master's OnExecuteDone fires:

pascal
// The REST request fetches data that contains embedded JSONTina4RESTRequest1.EndPoint := '/dashboard';Tina4RESTRequest1.MemTable := FDMemTableDashboard;โ€‹// The adapter extracts a nested array from the responseTina4JSONAdapter1.MasterSource := Tina4RESTRequest1;Tina4JSONAdapter1.DataKey := 'recentOrders';Tina4JSONAdapter1.MemTable := FDMemTableOrders;// When Tina4RESTRequest1 completes, FDMemTableOrders auto-populates

This works well for APIs that return complex nested responses. The REST request gets the whole response into one MemTable. The JSON adapter extracts a specific nested array into another MemTable.

Sync Mode#

By default, Execute clears the MemTable and replaces all data. For incremental updates, use Sync mode:

pascal
Tina4JSONAdapter1.SyncMode := TTina4RestSyncMode.Sync;Tina4JSONAdapter1.IndexFieldNames := 'id';
Sync ModeBehavior
Clear (default)Empties the table first, then appends all records
SyncMatches records by IndexFieldNames, updates existing rows, inserts new ones

Sync mode requires IndexFieldNames to be set. This is the field (or fields) used to match existing rows against incoming JSON records. Without it, sync mode cannot determine which rows to update.


3. Database to JSON#

Going the other direction -- from database to JSON -- is equally common. You query a database and need to send the results to a REST API, save to a file, or display in an HTML template.

GetJSONFromDB#

Executes a SQL query and returns the results as a TJSONObject. Three automatic conversions happen:

  1. Field names convert from snake_case to camelCase (e.g., first_name becomes firstName)
  2. DateTime fields format as ISO 8601 (e.g., 2024-06-15T14:30:00.000Z)
  3. Blob fields encode as Base64
pascal
// Simple queryvar Result := GetJSONFromDB(FDConnection1, 'SELECT * FROM users');try  Memo1.Lines.Text := Result.Format;  // {"records": [  //   {"id": "1", "firstName": "Alice", "email": "alice@example.com", ...},  //   {"id": "2", "firstName": "Bob", "email": "bob@example.com", ...}  // ]}finally  Result.Free;end;

The default dataset key is records. To use a custom key:

pascal
var Result := GetJSONFromDB(FDConnection1,  'SELECT * FROM cats', nil, 'cats');// {"cats": [{"id": "1", "name": "Whiskers"}, ...]}

With Parameters#

Use TFDParams for parameterized queries to prevent SQL injection:

pascal
var Params := TFDParams.Create;try  Params.Add('status', 'active');  Params.Add('minAge', 18);โ€‹  var Result := GetJSONFromDB(FDConnection1,    'SELECT * FROM users WHERE status = :status AND age >= :minAge',    Params);  try    Memo1.Lines.Text := Result.Format;  finally    Result.Free;  end;finally  Params.Free;end;

GetJSONFromTable#

Converts an existing TFDMemTable or TFDTable to JSON. Useful when you have data already loaded and need to serialize it:

pascal
// Basic conversionvar JSON := GetJSONFromTable(FDMemTable1);try  Memo1.Lines.Text := JSON.Format;  // {"records": [{"id": "1", "name": "Item1"}, ...]}finally  JSON.Free;end;

Ignore specific fields (passwords, internal IDs):

pascal
var JSON := GetJSONFromTable(FDMemTable1, 'records', 'password,internal_id');

Ignore blank values to reduce payload size:

pascal
var JSON := GetJSONFromTable(FDMemTable1, 'records', '', True);// Fields with empty string values are omitted from each record

4. JSON to MemTable#

The reverse pipeline. You have JSON data and need it in a TFDMemTable for display, editing, or further processing.

GetFieldDefsFromJSONObject#

Creates field definitions on a MemTable from a JSON object's structure. You call this once to set up the schema, then populate rows:

pascal
var JSONObj := StrToJSONObject(  '{"firstName": "Alice", "age": 30, "address": {"city": "Cape Town"}}');try  GetFieldDefsFromJSONObject(JSONObj, FDMemTable1, True);  // Creates fields:  //   first_name : ftString  (camelCase converted to snake_case with True flag)  //   age        : ftString  //   address    : ftMemo    (nested object becomes ftMemo)  FDMemTable1.CreateDataSet;finally  JSONObj.Free;end;

The third parameter controls snake_case conversion. Pass True to convert firstName to first_name. Pass False to keep JSON field names as-is.

Nested objects and arrays become ftMemo fields containing the serialized JSON string.

PopulateMemTableFromJSON#

The main workhorse. Takes a JSON string, extracts the array at the specified data key, and populates a MemTable. If the MemTable has no field definitions, they are created automatically from the first JSON object.

Clear Mode (Default)#

Empties the table and replaces all data:

pascal
var JSONStr :=  '{"records": [' +  '  {"id": "1", "name": "Alice", "email": "alice@example.com"},' +  '  {"id": "2", "name": "Bob", "email": "bob@example.com"}' +  ']}';โ€‹PopulateMemTableFromJSON(FDMemTable1, 'records', JSONStr);// FDMemTable1 has 2 rows, any previous data is gone

Sync Mode#

Matches existing rows by key fields and updates them. New rows are inserted. Existing rows not in the JSON are left unchanged:

pascal
// Initial loadPopulateMemTableFromJSON(FDMemTable1, 'records',  '{"records": [{"id": "1", "name": "Alice"}, {"id": "2", "name": "Bob"}]}');โ€‹// Later: update Alice, add Charlie, Bob stays unchangedPopulateMemTableFromJSON(FDMemTable1, 'records',  '{"records": [{"id": "1", "name": "Alice Updated"}, {"id": "3", "name": "Charlie"}]}',  'id', TTina4RestSyncMode.Sync);โ€‹// Result: 3 rows//   id=1: Alice Updated (updated)//   id=2: Bob (unchanged)//   id=3: Charlie (inserted)

The fourth parameter is IndexFieldNames -- the field(s) used for matching. For composite keys, separate with semicolons: 'tenantId;userId'.

PopulateTableFromJSON#

Inserts or updates rows directly into a database table (not a MemTable) from JSON. Uses a primary key for upsert logic:

pascal
var Result := PopulateTableFromJSON(  FDConnection1,           // database connection  'users',                 // table name  '{"response": [{"name": "Alice"}, {"name": "Bob"}]}',  'response',              // data key  'id');                   // primary key field for upsert

This is useful for bulk imports -- JSON data goes directly to the database without an intermediate MemTable.


5. Naming Conventions#

Tina4 Delphi automatically converts between naming conventions at every boundary:

DirectionFromToExample
Database to JSONsnake_casecamelCasefirst_name becomes firstName
JSON to MemTablecamelCasesnake_case (optional)firstName becomes first_name

CamelCase#

pascal
CamelCase('first_name');     // 'firstName'CamelCase('id');             // 'id'CamelCase('user_email');     // 'userEmail'CamelCase('created_at');     // 'createdAt'

SnakeCase#

pascal
SnakeCase('firstName');      // 'first_name'SnakeCase('userEmail');      // 'user_email'SnakeCase('createdAt');      // 'created_at'

This matters because databases typically use snake_case column names while JSON APIs use camelCase keys. Tina4 handles the translation transparently when using GetJSONFromDB and GetFieldDefsFromJSONObject.


6. Complete Example: Data Import/Export Tool#

A realistic scenario: fetch data from an API, display it in a grid, let the user edit rows, and push changes back to the API.

pascal
unit ImportExport;โ€‹interfaceโ€‹uses  System.SysUtils, System.Classes, System.JSON,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Edit,  FMX.Grid, FMX.Grid.Style, FMX.ScrollBox, FMX.Memo, FMX.Layouts,  FireDAC.Comp.Client,  Tina4Core, Tina4REST, Tina4RESTRequest;โ€‹type  TfrmImportExport = class(TForm)    restAPI: TTina4REST;    mtData: TFDMemTable;    gridData: TStringGrid;    btnFetch: TButton;    btnPushChanges: TButton;    memoLog: TMemo;    lblStatus: TLabel;    procedure FormCreate(Sender: TObject);    procedure btnFetchClick(Sender: TObject);    procedure btnPushChangesClick(Sender: TObject);  private    procedure RefreshGrid;    procedure Log(const Msg: string);  end;โ€‹var  frmImportExport: TfrmImportExport;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TfrmImportExport.FormCreate(Sender: TObject);begin  restAPI.BaseUrl := 'https://jsonplaceholder.typicode.com';end;โ€‹procedure TfrmImportExport.btnFetchClick(Sender: TObject);var  StatusCode: Integer;  Response: TJSONObject;begin  Log('Fetching users...');โ€‹  Response := restAPI.Get(StatusCode, '/users');  try    if StatusCode <> 200 then    begin      Log('Failed: HTTP ' + StatusCode.ToString);      Exit;    end;โ€‹    // The response is a JSON array, but Tina4REST wraps it    // Use PopulateMemTableFromJSON for direct control    PopulateMemTableFromJSON(mtData, '', Response.ToString);    RefreshGrid;    Log(Format('Loaded %d users', [mtData.RecordCount]));  finally    Response.Free;  end;end;โ€‹procedure TfrmImportExport.btnPushChangesClick(Sender: TObject);var  StatusCode: Integer;  Response: TJSONObject;  JSON: TJSONObject;begin  // Serialize the MemTable to JSON  JSON := GetJSONFromTable(mtData);  try    Log('Pushing changes...');    Log('Payload: ' + JSON.ToString);โ€‹    // In a real app, POST this to your API    Response := restAPI.Post(StatusCode, '/users', '', JSON.ToString);    try      if StatusCode in [200, 201] then        Log('Changes pushed successfully')      else        Log('Push failed: HTTP ' + StatusCode.ToString);    finally      Response.Free;    end;  finally    JSON.Free;  end;end;โ€‹procedure TfrmImportExport.RefreshGrid;begin  gridData.RowCount := mtData.RecordCount;  gridData.ClearColumns;โ€‹  for var I := 0 to mtData.FieldCount - 1 do  begin    var Col := TStringColumn.Create(gridData);    Col.Header := mtData.Fields[I].FieldName;    Col.Width := 150;    gridData.AddObject(Col);  end;โ€‹  mtData.First;  var Row := 0;  while not mtData.Eof do  begin    for var C := 0 to mtData.FieldCount - 1 do      gridData.Cells[C, Row] := mtData.Fields[C].AsString;    Inc(Row);    mtData.Next;  end;end;โ€‹procedure TfrmImportExport.Log(const Msg: string);begin  memoLog.Lines.Add(FormatDateTime('hh:nn:ss', Now) + ' ' + Msg);end;โ€‹end.

7. Complete Example: Master-Detail Pattern#

Customers in the top grid. Orders for the selected customer in the bottom grid. The orders grid updates automatically when you select a different customer.

pascal
unit MasterDetail;โ€‹interfaceโ€‹uses  System.SysUtils, System.Classes, System.JSON,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls,  FMX.Grid, FMX.Grid.Style, FMX.ScrollBox, FMX.Layouts,  FireDAC.Comp.Client,  Tina4Core, Tina4REST, Tina4RESTRequest, Tina4JSONAdapter;โ€‹type  TfrmMasterDetail = class(TForm)    restAPI: TTina4REST;    reqCustomers: TTina4RESTRequest;    adapterOrders: TTina4JSONAdapter;    mtCustomers: TFDMemTable;    mtOrders: TFDMemTable;    gridCustomers: TStringGrid;    gridOrders: TStringGrid;    btnLoad: TButton;    lblCustomerCount: TLabel;    lblOrderCount: TLabel;    procedure FormCreate(Sender: TObject);    procedure btnLoadClick(Sender: TObject);    procedure gridCustomersSelectCell(Sender: TObject; const ACol, ARow: Integer;      var CanSelect: Boolean);  private    FOrdersData: TJSONObject;    procedure RefreshCustomerGrid;    procedure LoadOrdersForCustomer(CustomerId: string);    procedure RefreshOrderGrid;  end;โ€‹var  frmMasterDetail: TfrmMasterDetail;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TfrmMasterDetail.FormCreate(Sender: TObject);begin  restAPI.BaseUrl := 'https://api.example.com/v1';  restAPI.SetBearer('your-token-here');โ€‹  reqCustomers.Tina4REST := restAPI;  reqCustomers.EndPoint := '/customers';  reqCustomers.RequestType := TTina4RequestType.Get;  reqCustomers.DataKey := 'records';  reqCustomers.MemTable := mtCustomers;  reqCustomers.SyncMode := TTina4RestSyncMode.Clear;โ€‹  FOrdersData := nil;end;โ€‹procedure TfrmMasterDetail.btnLoadClick(Sender: TObject);begin  reqCustomers.ExecuteRESTCall;  RefreshCustomerGrid;  lblCustomerCount.Text := Format('%d customers', [mtCustomers.RecordCount]);โ€‹  // Auto-select first customer  if mtCustomers.RecordCount > 0 then  begin    mtCustomers.First;    LoadOrdersForCustomer(mtCustomers.FieldByName('id').AsString);  end;end;โ€‹procedure TfrmMasterDetail.gridCustomersSelectCell(Sender: TObject;  const ACol, ARow: Integer; var CanSelect: Boolean);begin  if ARow < mtCustomers.RecordCount then  begin    mtCustomers.RecNo := ARow + 1;    LoadOrdersForCustomer(mtCustomers.FieldByName('id').AsString);  end;end;โ€‹procedure TfrmMasterDetail.LoadOrdersForCustomer(CustomerId: string);var  StatusCode: Integer;  Response: TJSONObject;begin  Response := restAPI.Get(StatusCode,    '/customers/' + CustomerId + '/orders');  try    if StatusCode = 200 then    begin      PopulateMemTableFromJSON(mtOrders, 'records', Response.ToString);      RefreshOrderGrid;      lblOrderCount.Text := Format('%d orders', [mtOrders.RecordCount]);    end    else    begin      mtOrders.EmptyDataSet;      RefreshOrderGrid;      lblOrderCount.Text := '0 orders';    end;  finally    Response.Free;  end;end;โ€‹procedure TfrmMasterDetail.RefreshCustomerGrid;begin  gridCustomers.RowCount := mtCustomers.RecordCount;  gridCustomers.ClearColumns;โ€‹  var ColId := TStringColumn.Create(gridCustomers);  ColId.Header := 'ID';  ColId.Width := 50;  gridCustomers.AddObject(ColId);โ€‹  var ColName := TStringColumn.Create(gridCustomers);  ColName.Header := 'Name';  ColName.Width := 200;  gridCustomers.AddObject(ColName);โ€‹  var ColEmail := TStringColumn.Create(gridCustomers);  ColEmail.Header := 'Email';  ColEmail.Width := 250;  gridCustomers.AddObject(ColEmail);โ€‹  mtCustomers.First;  var Row := 0;  while not mtCustomers.Eof do  begin    gridCustomers.Cells[0, Row] := mtCustomers.FieldByName('id').AsString;    gridCustomers.Cells[1, Row] := mtCustomers.FieldByName('name').AsString;    gridCustomers.Cells[2, Row] := mtCustomers.FieldByName('email').AsString;    Inc(Row);    mtCustomers.Next;  end;end;โ€‹procedure TfrmMasterDetail.RefreshOrderGrid;begin  gridOrders.RowCount := mtOrders.RecordCount;  gridOrders.ClearColumns;โ€‹  for var I := 0 to mtOrders.FieldCount - 1 do  begin    var Col := TStringColumn.Create(gridOrders);    Col.Header := mtOrders.Fields[I].FieldName;    Col.Width := 120;    gridOrders.AddObject(Col);  end;โ€‹  mtOrders.First;  var Row := 0;  while not mtOrders.Eof do  begin    for var C := 0 to mtOrders.FieldCount - 1 do      gridOrders.Cells[C, Row] := mtOrders.Fields[C].AsString;    Inc(Row);    mtOrders.Next;  end;end;โ€‹end.

8. Exercise: JSON Viewer#

Build a universal JSON viewer that can load any JSON file, auto-create MemTable fields, display the data in a grid, and allow editing.

Requirements#

  1. An "Open File" button that loads a .json file from disk
  2. A TEdit for specifying the data key (default: records)
  3. Auto-detect field definitions from the JSON structure
  4. Display the data in a TStringGrid
  5. Allow the user to edit cells in the grid
  6. A "Save" button that writes the modified data back to the JSON file

Solution#

pascal
unit JSONViewer;โ€‹interfaceโ€‹uses  System.SysUtils, System.Classes, System.JSON, System.IOUtils,  FMX.Types, FMX.Controls, FMX.Forms, FMX.StdCtrls, FMX.Edit,  FMX.Grid, FMX.Grid.Style, FMX.ScrollBox, FMX.Dialogs, FMX.Layouts,  FireDAC.Comp.Client,  Tina4Core;โ€‹type  TfrmJSONViewer = class(TForm)    btnOpen: TButton;    btnSave: TButton;    edtDataKey: TEdit;    gridData: TStringGrid;    mtData: TFDMemTable;    lblStatus: TLabel;    lblDataKey: TLabel;    OpenDialog1: TOpenDialog;    SaveDialog1: TSaveDialog;    procedure btnOpenClick(Sender: TObject);    procedure btnSaveClick(Sender: TObject);    procedure FormCreate(Sender: TObject);  private    FCurrentFile: string;    FOriginalJSON: string;    procedure LoadJSON(const FileName: string);    procedure RefreshGrid;  end;โ€‹var  frmJSONViewer: TfrmJSONViewer;โ€‹implementationโ€‹{$R *.fmx}โ€‹procedure TfrmJSONViewer.FormCreate(Sender: TObject);begin  edtDataKey.Text := 'records';  OpenDialog1.Filter := 'JSON files (*.json)|*.json|All files (*.*)|*.*';  SaveDialog1.Filter := 'JSON files (*.json)|*.json';end;โ€‹procedure TfrmJSONViewer.btnOpenClick(Sender: TObject);begin  if OpenDialog1.Execute then    LoadJSON(OpenDialog1.FileName);end;โ€‹procedure TfrmJSONViewer.LoadJSON(const FileName: string);var  JSONStr: string;  DataKey: string;begin  FCurrentFile := FileName;  JSONStr := TFile.ReadAllText(FileName);  FOriginalJSON := JSONStr;  DataKey := edtDataKey.Text;โ€‹  // Clear existing data  mtData.Close;  mtData.FieldDefs.Clear;โ€‹  // Try to parse and detect structure  var JSONVal := StrToJSONValue(JSONStr);  try    if JSONVal is TJSONArray then    begin      // Root is an array -- wrap it for PopulateMemTableFromJSON      var Wrapped := Format('{"%s": %s}', [DataKey, JSONStr]);      PopulateMemTableFromJSON(mtData, DataKey, Wrapped);    end    else if JSONVal is TJSONObject then    begin      PopulateMemTableFromJSON(mtData, DataKey, JSONStr);    end    else    begin      lblStatus.Text := 'JSON is neither an object nor an array';      Exit;    end;  finally    JSONVal.Free;  end;โ€‹  RefreshGrid;  lblStatus.Text := Format('Loaded %d records from %s',    [mtData.RecordCount, ExtractFileName(FileName)]);end;โ€‹procedure TfrmJSONViewer.RefreshGrid;begin  gridData.RowCount := mtData.RecordCount;  gridData.ClearColumns;โ€‹  for var I := 0 to mtData.FieldCount - 1 do  begin    var Col := TStringColumn.Create(gridData);    Col.Header := mtData.Fields[I].FieldName;    Col.Width := 150;    gridData.AddObject(Col);  end;โ€‹  mtData.First;  var Row := 0;  while not mtData.Eof do  begin    for var C := 0 to mtData.FieldCount - 1 do      gridData.Cells[C, Row] := mtData.Fields[C].AsString;    Inc(Row);    mtData.Next;  end;end;โ€‹procedure TfrmJSONViewer.btnSaveClick(Sender: TObject);var  JSON: TJSONObject;  FileName: string;begin  // Read grid edits back into MemTable  mtData.First;  var Row := 0;  while not mtData.Eof do  begin    mtData.Edit;    for var C := 0 to mtData.FieldCount - 1 do      mtData.Fields[C].AsString := gridData.Cells[C, Row];    mtData.Post;    Inc(Row);    mtData.Next;  end;โ€‹  // Serialize to JSON  JSON := GetJSONFromTable(mtData, edtDataKey.Text);  try    if FCurrentFile <> '' then      FileName := FCurrentFile    else if SaveDialog1.Execute then      FileName := SaveDialog1.FileName    else      Exit;โ€‹    TFile.WriteAllText(FileName, JSON.Format);    lblStatus.Text := 'Saved to ' + ExtractFileName(FileName);  finally    JSON.Free;  end;end;โ€‹end.

9. Common Gotchas#

TJSONObject Memory Management#

Symptom: Memory leaks reported by ReportMemoryLeaksOnShutdown.

Fix: Every function that returns a TJSONObject -- StrToJSONObject, BytesToJSONObject, GetJSONFromDB, GetJSONFromTable, Get, Post, etc. -- creates an object on the heap. You must free it:

pascal
// Pattern: always use try..finallyvar Obj := StrToJSONObject(SomeString);try  // work with Objfinally  Obj.Free;end;

Do not free child objects extracted with GetValue<TJSONObject> or GetValue<TJSONArray> -- they are owned by the parent. Freeing the parent frees all children.

Nested JSON Becoming ftMemo Fields#

Symptom: A field contains {"city": "Cape Town", "zip": "8001"} instead of the expected flat value.

Explanation: When GetFieldDefsFromJSONObject encounters a nested JSON object or array, it creates an ftMemo field containing the serialized JSON string. This is by design -- there is no automatic flattening.

Fix: If you need flat fields, pre-process the JSON to flatten it before populating the MemTable. Or use a second TTina4JSONAdapter to extract nested data into a separate MemTable.

Sync Mode Without IndexFieldNames#

Symptom: Duplicate rows appear in the MemTable after sync.

Fix: When using TTina4RestSyncMode.Sync, you must set IndexFieldNames. Without it, the sync has no way to match incoming records to existing rows, so it appends everything:

pascal
// WRONG -- no index, sync inserts duplicatesPopulateMemTableFromJSON(mtData, 'records', JSONStr,  '', TTina4RestSyncMode.Sync);โ€‹// CORRECT -- match by id fieldPopulateMemTableFromJSON(mtData, 'records', JSONStr,  'id', TTina4RestSyncMode.Sync);

DataKey Does Not Exist#

Symptom: MemTable is empty after PopulateMemTableFromJSON, even though the JSON contains data.

Fix: Verify the data key matches the actual JSON structure. Common mismatches:

pascal
// API returns {"data": [...]}PopulateMemTableFromJSON(mtData, 'records', JSONStr);  // WRONG: no "records" keyPopulateMemTableFromJSON(mtData, 'data', JSONStr);     // CORRECTโ€‹// API returns a bare array [...]PopulateMemTableFromJSON(mtData, 'records', JSONStr);   // WRONG: no wrapper object// Wrap it first:var Wrapped := '{"records": ' + JSONStr + '}';PopulateMemTableFromJSON(mtData, 'records', Wrapped);   // CORRECT

Date Fields Not Parsing#

Symptom: Date values appear as raw strings like 2024-06-15T14:30:00.000Z instead of TDateTime values.

Explanation: PopulateMemTableFromJSON creates all fields as ftString by default (auto-detected from JSON, which has no date type). Dates are stored as strings.

Fix: Use IsDate and JSONDateToDateTime for explicit conversion:

pascal
if IsDate(mtData.FieldByName('createdAt').AsString) thenbegin  var DT := JSONDateToDateTime(mtData.FieldByName('createdAt').AsString);  // DT is now a TDateTime you can format or compareend;

Summary#

WhatHow
Parse JSON stringStrToJSONObject(str) / StrToJSONArray(str)
Parse HTTP responseBytesToJSONObject(bytes)
JSON adapterTTina4JSONAdapter -- set MemTable, DataKey, JSONData, Execute
Adapter from RESTSet MasterSource to a TTina4RESTRequest
Sync modeSyncMode := Sync + IndexFieldNames := 'id'
DB to JSONGetJSONFromDB(Connection, SQL) -- auto camelCase, ISO dates
Table to JSONGetJSONFromTable(MemTable)
JSON to MemTablePopulateMemTableFromJSON(MemTable, DataKey, JSON)
JSON to DBPopulateTableFromJSON(Connection, TableName, JSON, DataKey, PK)
Field defs from JSONGetFieldDefsFromJSONObject(JSONObj, MemTable, SnakeCase)
camelCase convertCamelCase('snake_name')
snake_case convertSnakeCase('camelName')
Date checkIsDate(Value)
Date to ISOGetJSONDate(DateTime)
ISO to DateJSONDateToDateTime(ISOString)