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.
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:
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:
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:
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:
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#
// 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 columnsFrom MasterSource#
Link the adapter to a TTina4RESTRequest and it auto-executes whenever the master's OnExecuteDone fires:
// 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-populatesThis 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:
Tina4JSONAdapter1.SyncMode := TTina4RestSyncMode.Sync;Tina4JSONAdapter1.IndexFieldNames := 'id';| Sync Mode | Behavior |
|---|---|
Clear (default) | Empties the table first, then appends all records |
Sync | Matches 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:
- Field names convert from
snake_casetocamelCase(e.g.,first_namebecomesfirstName) - DateTime fields format as ISO 8601 (e.g.,
2024-06-15T14:30:00.000Z) - Blob fields encode as Base64
// 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:
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:
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:
// 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):
var JSON := GetJSONFromTable(FDMemTable1, 'records', 'password,internal_id');Ignore blank values to reduce payload size:
var JSON := GetJSONFromTable(FDMemTable1, 'records', '', True);// Fields with empty string values are omitted from each record4. 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:
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:
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 goneSync Mode#
Matches existing rows by key fields and updates them. New rows are inserted. Existing rows not in the JSON are left unchanged:
// 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:
var Result := PopulateTableFromJSON( FDConnection1, // database connection 'users', // table name '{"response": [{"name": "Alice"}, {"name": "Bob"}]}', 'response', // data key 'id'); // primary key field for upsertThis 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:
| Direction | From | To | Example |
|---|---|---|---|
| Database to JSON | snake_case | camelCase | first_name becomes firstName |
| JSON to MemTable | camelCase | snake_case (optional) | firstName becomes first_name |
CamelCase#
CamelCase('first_name'); // 'firstName'CamelCase('id'); // 'id'CamelCase('user_email'); // 'userEmail'CamelCase('created_at'); // 'createdAt'SnakeCase#
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.
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.
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#
- An "Open File" button that loads a
.jsonfile from disk - A
TEditfor specifying the data key (default:records) - Auto-detect field definitions from the JSON structure
- Display the data in a
TStringGrid - Allow the user to edit cells in the grid
- A "Save" button that writes the modified data back to the JSON file
Solution#
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:
// 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:
// 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:
// 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); // CORRECTDate 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:
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#
| What | How |
|---|---|
| Parse JSON string | StrToJSONObject(str) / StrToJSONArray(str) |
| Parse HTTP response | BytesToJSONObject(bytes) |
| JSON adapter | TTina4JSONAdapter -- set MemTable, DataKey, JSONData, Execute |
| Adapter from REST | Set MasterSource to a TTina4RESTRequest |
| Sync mode | SyncMode := Sync + IndexFieldNames := 'id' |
| DB to JSON | GetJSONFromDB(Connection, SQL) -- auto camelCase, ISO dates |
| Table to JSON | GetJSONFromTable(MemTable) |
| JSON to MemTable | PopulateMemTableFromJSON(MemTable, DataKey, JSON) |
| JSON to DB | PopulateTableFromJSON(Connection, TableName, JSON, DataKey, PK) |
| Field defs from JSON | GetFieldDefsFromJSONObject(JSONObj, MemTable, SnakeCase) |
| camelCase convert | CamelCase('snake_name') |
| snake_case convert | SnakeCase('camelName') |
| Date check | IsDate(Value) |
| Date to ISO | GetJSONDate(DateTime) |
| ISO to Date | JSONDateToDateTime(ISOString) |