Create tree structured JSON - json

I have a list of objects, of type TDepartment which looks like this
TDepartment = class
ID : Integer;
Name : string;
ParentDepartmentID : Integer;
end;
I need to create a TJSONObject, with an array of departments, which all can also have an array of departments. So the depth of this is unknown.
I am at a point right now where it simply doesn't make sense to me, but I would like the resulting JSON to look like this:
"department_id": "5",
"department_name": "100",
"parent_dept_id": "",
"subdepartments": [{
"department_id": "8",
"department_name": "300",
"parent_dept_id": "5",
"subdepartments": [{
"department_id": "1",
"department_name": "310",
"parent_dept_id": "8",
"subdepartments": []
Keep in mind that each level has unknown amount of siblings and children.
I guess i need to write a recursive procedure, but I am unable to visualize it.

First, you probably want your declaration of TDepartmentto match the nested structure you describe:
TDepartment = class
ID : Integer;
Name : string;
ParentDepartmentID : Integer;
SubDepartments: array of TDepartment;
end;
In order to serialize this I would recommend using the SuperObject library rather than the inbuilt JSON classes:
function TDepartment.Serialize: ISuperObject;
var Context: TSuperRttiContext;
begin
Context := TSuperRttiContext.Create;
try
Result := Context.AsJson<TDepartment>(self);
finally
Context.Free;
end;
end;
In the comments, OP mentioned that TDepartment contains a lot more fields, but only the ones in the question should be serialized; also TJSONObject has to be used, and a department does not know about its children. You could do something like that:
function TDepartment.Serialize2(AllDepartments: TList<TDepartment>): TJSONObject;
var Department: TDepartment;
Subdepartments: TJSONArray;
begin
Result := TJSONObject.Create;
Result.AddPair(TJSONPair.Create('department_id', TJSONNumber.Create(ID)));
Result.AddPair(TJSONPair.Create('department_name', Name));
Result.AddPair(TJSONPair.Create('parent_dept_id', TJSONNumber.Create(ParentDepartmentID)));
Subdepartments := TJSonArray.Create;
for Department in AllDepartments do
begin
if (Department.ParentDepartmentID <> ID) then Continue;
Subdepartments.AddElement(Department.Serialize2(AllDepartments));
end;
Result.AddPair(TJSONPair.Create('subdepartments', Subdepartments));
end;

I would create a parallel tree structure leaving the original intact. Your current structure is inverted as to what you need, so you scan through your current objects placing them in the tree. But without knowing the current structure this is difficult give sample code, but assuming all departments exist in some sort of list, (let us say called 'Departments') and the 'root' department has a parent department ID of zero it would go something like this:
unit Unit1;
interface
uses
System.Generics.Collections;
type
TDepartment = class
ID : Integer;
Name : string;
ParentDepartmentID : Integer;
end;
TDepartmentStructure = class
ID : Integer;
Name : string;
ParentDepartmentID : Integer;
SubDepartments: TList< TDepartmentStructure >;
constructor Create( const pBasedOn : TDepartment );
end;
var
Department : TObjectList<TDepartment>;
function CopyStructure( pDepartment : TList<TDepartment> ) : TDepartmentStructure; // returns root
implementation
var
DepartmentStructure : TObjectList<TDepartmentStructure>;
function CopyStructure( pDepartment : TList<TDepartment> ) : TDepartmentStructure;
var
i, j: Integer;
begin
// stage one - copy everything
for i := 0 to pDepartment.Count - 1 do
begin
DepartmentStructure.Add( TDepartmentStructure.Create( pDepartment[ i ] ));
end;
// now go through and build structure
Result := nil;
for i := 0 to DepartmentStructure.Count - 1 do
begin
if DepartmentStructure[ i ].ID = 0 then
begin
// root
Result := DepartmentStructure[ i ];
end
else
begin
for j := 0 to DepartmentStructure.Count - 1 do
begin
if DepartmentStructure[ i ].ParentDepartmentID = DepartmentStructure[ j ].ID then
begin
DepartmentStructure[ j ].SubDepartments.Add( DepartmentStructure[ i ] );
break;
end;
end;
end;
end;
end;
{ TDepartmentStructure }
constructor TDepartmentStructure.Create(const pBasedOn: TDepartment);
begin
inherited Create;
ID := pBasedOn.ID;
Name := pBasedOn.Name;
ParentDepartmentID := pBasedOn.ParentDepartmentID;
SubDepartments:= TObjectList< TDepartmentStructure >.Create( FALSE ); // we do NOT own these objects!
end;
initialization
DepartmentStructure := TObjectList<TDepartmentStructure>.Create( TRUE );
finalization
DepartmentStructure.Free;
end.
Note that this is for illustration purposes only. You would probably not create and destroy the structures where I have. Once you have the structure you can create your JSON records using your current routines no doubt.

Related

Delphi JsonTextReader fails to read values

I have a very weird situation.
This is the JSON I am trying to parse:
[
{
"username":"xxx",
"email":"xxx#gmail.com",
"custom_title":"xxx title",
"timezone":"Africa\/Cairo",
"message_count":"218",
"alerts_unread":"0",
"like_count":"385",
"friend_count":"0"
}
]
This is my parsing code:
type
TUserData = record
email, timezone: string;
msg, alerts, likes: integer;
end;
procedure TDMRest.parseData(var b: TUserData);
var
jtr: TJsonTextReader;
sr: TStringReader;
begin
//RESTResponseLogin.Content has the above json text
sr := TStringReader.Create(RESTResponseLogin.Content);
try
jtr := TJsonTextReader.Create(sr);
try
while jtr.Read do
begin
if jtr.TokenType = TJsonToken.StartObject then
process(b, jtr);
end;
finally
jtr.Free;
end;
finally
sr.Free;
end;
end;
//here there is a problem
procedure TDMRest.process(var c: TUserData; jtr: TJsonTextReader);
begin
while jtr.Read do
begin
if (jtr.TokenType = TJsonToken.PropertyName) then
begin
if jtr.Value.ToString = 'email' then
begin
jtr.Read;
c.email := jtr.Value.AsString;
end;
if jtr.Value.ToString = 'timezone' then
begin
jtr.Read;
c.timezone := jtr.Value.AsString;
end;
if jtr.Value.ToString = 'message_count' then
begin
jtr.Read;
c.msg := jtr.Value.AsInteger;
end;
if jtr.TokenType = TJsonToken.EndObject then
begin
c.alerts := 0;
c.likes := 0;
exit;
end;
end;
end;
end;
MY PROBLEM: In the process() code, the first 2 if blocks (email and timezone) can read values into my record. But when I add other if blocks (like if jtr.Value.ToString = 'message_count' then), I cannot see the values of my record anymore.
Am I parsing the data properly?
Basically, I need to grab the info from a JSON string and put the data inside a TUserData record.
I have found the above pattern in a book titled "Expert Delphi", and I am pretty sure that the parseData() function is correct. Probably I am missing something in the process.
The TDMRrst is a DataModule; I am giving the function a record, and I'd like the data to be properly parsed.
What is wrong here?
In the JSON you have shown, all of the values are strings, there are no integers. So, when you call jtr.Value.AsInteger for the message_count value, it raises a conversion exception that you are not catching. TValue.AsInteger DOES NOT perform an implicit conversion from string to integer for you.
You will have to use jtr.Value.AsString instead and convert the string to an integer using StrToInt():
if jtr.Value.ToString = 'message_count' then
begin
jtr.Read;
//c.msg := jtr.Value.AsInteger;
c.msg := StrToInt(jtr.Value.AsString);
end;
Do the same for the other "integer" values in the JSON (alerts_unread, like_count, and friend_count).

How correct to parse following JSON document including array

JSON-Document:
{"asks":
[["0.01129999",0.9997237],["0.01130000",594.26412711],["0.01130826",0.23004724],["0.01130999",0.25231793]],
"bids":
[["0.01129800",174.30861783],["0.01128867",0.13287659],["0.01128817",0.0097447],["0.01127559",241.94881004]],
"isFrozen":"0","seq":254407265
}
This is my first expirience of work with JSON-documents. I'm done that as I may. It work. But I think that it was wrong way. Because in my realization absent TJSONPair class, unlike many examples in net. Is there other variant of realisation comprising TJSONPair in my case?
const
ArrString : array[0..1] of String = ('asks','bids');
var
JSONObject: TJSONObject;
JSONArray: TJSONArray;
i,j: integer;
...
JSONObject:=TJSONObject.ParseJSONValue(JSONString) as TJSONObject;
if Assigned(JSONObject) then
begin
for i := 0 to High(ArrString) do
begin
JSONArray := JSONObject.Get(ArrString[i]).JsonValue as TJSONArray;
for j := 0 to JSONArray.Size-1 do
begin
if j = 0 then Memo1.Lines.Add(ArrString[i]+':');
Memo1.Lines.Add(TJSONString(TJSONArray(JSONArray.Get(j)).Get(0)).Value+ ' ' +TJSONNumber(TJSONArray(JSONArray.Get(j)).Get(1)).ToString);
end;
end;
I would advise you to ditch the inbuilt JSON classes, and instead use the excellent SuperObject classes from Henry Gourvest. They've been around longer, are better tested, more powerful, and easier to use.
With those, your code would become:
procedure ParseJSON(const JSONString: string);
const ArrString: array of string = ['asks', 'bids'];
var JSON, Element: ISuperObject;
CurrentString: string;
begin
JSON := SO(JSONString);
for CurrentString in ArrString do
begin
Memo1.Lines.Add(CurrentString + ':');
for Element in JSON[CurrentString] do
Memo1.Lines.Add(Element.AsArray.S[0] + ' ' + Element.AsArray.D[1].ToString);
end;
end;

How add one object and one pair for a existent .json file?

I have a code that changes a value of a determinated pair in a existent JSON file and works perfectly. Now i need add one object and one pair to this file, using great part this same code. So how do this?
Thank you.
uses
System.Json, ShFolder, System.IOUtils;
...
function GetSpecialFolderPath(folder : integer) : string;
const
SHGFP_TYPE_CURRENT = 0;
var
path: array [0..MAX_PATH] of char;
begin
if SUCCEEDED(SHGetFolderPath(0,folder,0,SHGFP_TYPE_CURRENT,#path[0])) then
Result := path
else
Result := '';
end;
procedure ChangeChromeSetting(const ATarget, Avalue: string);
var
specialfolder: integer;
caminhochrome: String;
JSONObj, Obj: TJSONObject;
JSONPair: TJSONPair;
OldValue: string;
begin
specialFolder := CSIDL_LOCAL_APPDATA;
caminhochrome := GetSpecialFolderPath(specialFolder);
caminhochrome := caminhochrome + '\Google\Chrome\User Data\Local State';
if fileexists(caminhochrome) then
begin
Obj := TJSONObject.Create;
JSONObj := TJSONObject.ParseJSONValue(TFile.ReadAllText(caminhochrome)) as TJSONObject;
if not Assigned(JSONObj) then raise Exception.Create('Cannot read file: ' + caminhochrome);
try
OldValue := JSONObj.GetValue<string>(ATarget);
if not SameText(OldValue, Avalue) then
begin
JSONPair := JSONObj.Get(ATarget);
JSONPair.JsonValue.Free;
JSONPair.JsonValue := TJSONString.Create(Avalue);
///////////////////////////////////////////////////
Obj.AddPair('enabled', TJSONBool.Create(false)); // Trying add pair
JSONObj.AddPair('hardware_acceleration_mode', Obj); // Trying add object
//////////////////////////////////////////////////
TFile.WriteAllText(caminhochrome, JSONObj.ToJSON); // Don't add object and pair
end;
finally
JSONObj.Free;
end;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
ChangeChromeSetting('hardware_acceleration_mode_previous', 'false');
end;
This is result that i'm waiting
"hardware_acceleration_mode":{"enabled":false}
Your code is somewhat confusing since you pass in some of the names as arguments, but then hard code others inside the function. Abstracting functionality is good practise but before you can abstract you really need to ensure the code works correctly. I'm going to show code that does not attempt to be abstract. Once you are satisfied it behaves as you need, then feel free to abstract away.
This code does what I believe is your intent:
var
root: TJSONObject;
value: TJSONObject;
prev: string;
begin
root := TJSONObject.ParseJSONValue(TFile.ReadAllText(FileName)) as TJSONObject;
try
prev := root.GetValue<string>('hardware_acceleration_mode_previous');
if not SameText(prev, 'false') then
begin
// remove existing value, if it exists
root.RemovePair('hardware_acceleration_mode').Free;
// create a new object, and initialise it
value := TJSONObject.Create;
value.AddPair('enabled', 'false');
// add the object at the root level
root.AddPair('hardware_acceleration_mode', value);
// save to file
TFile.WriteAllText(FileName, root.ToJSON);
end;
finally
root.Free;
end;
end;
Note that I have ensured that there are no memory leaks. I've used RemovePair to make sure that if there is an existing value named hardware_acceleration_mode it is first removed.

Get key names in json file in delphi

I have a json string format like this:
{
"LIST":{
"Joseph":{
"item1":0,
"item2":0
},
"John":{
"item1":0,
"item2":0
},
"Fred":{
"item1":0,
"item2":0
}
}
}
I need to get the names, "Joseph", "John", "Fred" and so on... I have a function that will add names to the list, I have no idea what names will be added so I need to get those names.
I can only get the name "LIST" with this code:
js := TlkJSONstreamed.loadfromfile(jsonFile) as TlkJsonObject;
try
ShowMessage( vartostr(js.NameOf[0]) );
finally
s.free;
end;
I'm using lkJSON-1.07 in delphi 7
You can get the names in turn and obtain the next object for each name.
Get the name: js.NameOf[0]
Obtain the object from the name: js[js.NameOf[0]]
The getJSONNames procedure prints all the names contained in a TlkJSONobject object recursively.
procedure getJSONNames(const Ajs: TlkJSONobject);
var
i: Integer;
begin
if Ajs = nil then
Exit
else
for i := 0 to Ajs.Count-1 do begin
WriteLn(Ajs.NameOf[i]);
getJSONNames(TlkJSONobject(Ajs[Ajs.NameOf[i]]));
end;
end;
var
js: TlkJsonObject;
begin
js := TlkJSONstreamed.loadfromfile(jsonFile) as TlkJsonObject;
try
getJSONNames(js);
finally
js.free;
end;
end.

How to pass dynamic arrays in FreePascal correctly

In this FreePascal code I write, I found that in a dynamic array of length 'n', it always contained a random value in element 'n'.
I understand why that is, however, I am wondering if perhaps there is a flaw in the way i've written my code. I've pasted it below. I'd welcome any corrections.
program LinearSearch;
{$mode objfpc}{$H+}
uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
{$ENDIF}{$ENDIF}
Classes, SysUtils
{ you can add units after this };
{$R *.res}
type
ArrayOfByte = array of Byte;
Function findMaximum(studentMarks : ArrayOfByte; numberOfStudents : integer)
: integer;
var
maximumMark : Integer;
studentWithMaximumMark : Integer;
index : integer;
begin
setLength(studentMarks, numberOfStudents);
maximumMark := studentMarks[0];
for index:=0 to numberOfStudents do
begin
write(IntToStr(studentMarks[index]));
if index = numberOfStudents then writeln('.')
else write(',');
end;
for index:= 0 to (numberOfStudents - 1) do
begin
if studentMarks[index] > maximumMark then
begin
maximumMark := studentMarks[index];
studentWithMaximumMark := index;
end;
end;
write('Maximum score of ',IntToStr(maximumMark),' was achieved by ');
Result := studentWithMaximumMark+1;
end;
var
studentMarks : ArrayOfByte;
numberOfStudents : Integer;
studentWithMaximumMarks : Integer;
counter : integer;
begin
write('Enter the number of students: ' );
readln(numberOfStudents);
setLength(studentMarks,numberOfStudents);
writeln('Input the grades for the following students:');
for counter := 0 to (numberOfStudents - 1) do
begin
write('Student ',counter+1,': ');
readln(studentMarks[counter]);
end;
writeln('Data has been input. Finding student with maximum marks.');
studentWithMaximumMarks := findMaximum(studentMarks,numberOfStudents);
write('student no. ',IntToStr(studentWithMaximumMarks));
writeln;
writeln('Press ANY key to continue');
readln;
end.
In Free Pascal dynamic arrays are zero based. So the first element is MyArray[0] and last element is MyArray[High(MyArray)] which is equal to MyArray[Length(MyArray) - 1]. You should never access MyArray[Length(MyArray)].
...I found that in a dynamic array of length 'n', it always contained a random value in element 'n'. this is because you are accessing a value beyond of the bounds of the array, check in the procedure findMaximum the line
for index:=0 to numberOfStudents do
must be
for index:=0 to numberOfStudents-1 do
Also you can remove the first line of the same procedure setLength(studentMarks, numberOfStudents);