I get JSON from API and it have a quirk: usually it returns "tags" element as object {"x":"y"}, but if ther are no tags, it returns empty array [] instead.
I parse JSON with SuperObject, and use this code:
var
JsonObject: ISuperObject;
item: TSuperAvlEntry;
temp: TStringList;
begin
{...}
for item in JsonObject.O['tags'].AsObject do
begin
temp.Add(item.Name);
end;
{...}
It works wonderfully for objects, but it crashes with Access Violation error if it's an array.
As well, if I try something like:
if JSONObject['tags'].AsArray.Length=0 then
it works fine for empty array, but crashes if it is an object.
I don't know for sure that elements may be in "tags" and thus don't know how can I use Exists() in this case.
Any ideas?
Well, looks like I found the answer myself, so I will share it.
ISuperObject has a property "DataType" which you can check, like this:
if JsonObject['tags'].DataType = stObject then
begin
for item in JsonObject.O['tags'].AsObject do
begin
temp.Add(item.Name);
end;
end;
stObject and stArray are most useful to check, but there's also: stBoolean, stDouble, stCurrency, stInt and stMethod.
Related
We are using the Spring4D nullable types (which are records, not objects) in some of our business objects that need to be parsed to JSON.
When the nullable type field has no value, there are 2 options that would be okay in our case:
The field is not present in the JSON
The field is present in the JSON with value null
I am trying to make this work by using a TJSONInterceptor subclass.
For example for the TNullableInteger:
I want to create an interceptor that will be used when the field has a NullableIntegerAttribute (derived from JsonReflectAttribute), in which case my TNullableIntegerInterceptor will be used.
The problem is that I don't quite know which convertertype and revertertype to use in this case because the nullable types are record types and not object types.
Does anyone have any experience with parsing record types in Delphi?
Or are there other ways to achieve this?
Any guidance would be much appreciated.
UPDATE
In the meantime I have managed to make interceptors for most of the TNullable types that we use.
Example for TNullableInteger:
{ JsonNullableIntegerAttribute }
constructor JsonNullableIntegerAttribute.Create;
begin
inherited Create(ctObject, rtString, TNullableIntegerInterceptor);
end;
{ TNullableIntegerInterceptor }
constructor TNullableIntegerInterceptor.Create;
begin
inherited;
ConverterType := ctObject;
ReverterType := rtString;
end;
function TNullableIntegerInterceptor.ObjectConverter(Data: TObject; Field: string): TObject;
var
ctx: TRTTIContext;
Int: TNullableInteger;
begin
Int := ctx.GetType(Data.ClassType).GetField(Field).GetValue(Data).AsType<TNullableInteger>;
If Int.HasValue
then begin
StringProxy.Value := IntToStr(Int.GetValueOrDefault);
Result := StringProxy;
end
else Result := nil;
end;
procedure TNullableIntegerInterceptor.StringReverter(Data: TObject; Field, Arg: string);
var
ctx: TRTTIContext;
Value: TValue;
Int: Integer;
begin
If TryStrToInt(Arg, Int)
then begin
TValue.Make<TNullableInteger>(Int, Value);
ctx.GetType(Data.ClassType).GetField(Field).SetValue(Data, Value);
end;
end;
The only type that is still a problem is TNullableBoolean.
For the unmarshalling the procedure TJONUnMarshal.PopulateFields checks if there is a reverter available but then, depending on the revertertype, it only handles TJSONArray, TJSONObject, TJSONString and TJSONNull. Boolean values in JSON are of type TJSONBool and are not handled. Instead I get the error 'JSON object expected for field fieldname in JSON true'
Also, for the serialization, I don't know how I can achieve that my JSON looks like
"FieldName": true
instead of
"FieldName": "true"
Passing a boolean as a string in JSON is not allowed by our backend software. So for now I am using a rather stupid workaround in my ObjectConverter method by returning "BOOLEAN_TRUE_IDENTIFIER" or "BOOLEAN_FALSE_INDENTIFIER" which I replace by true or false in the resulting JSON just before sending it to our backend.
If anyone would have a solution for this, it would make me very happy :-)
In the meantime we will try to avoid using NullableBoolean variables.
I have a JSON array of objects that I iterate. From each element of the array I retrieve further json data via a function that returns a JSONValue. I then must add that returned jsonvalue to the specific array element.
It works perfectly except that Delphi is reporting a memory leak on shutdown and I just can't find it. I'm pretty new to JSON and I've spent all day on this. Can someone help by suggesting the right way to go about this. Simplified code below. Many Thanks.
function TForm1.Function1(ThisProductJSON: String): Integer;
Var
MultiObj : TJSONObject;
OtherValues : TJSONVALUE;
ThisObject : TJSONObject;
I : Integer;
jsArr : TJSONARRAY;
S:String;
Stockcode : String;
begin
MultiObj:=TJSONObject.Create(nil);
s:='[{"ThisElement":1,"Thiscode":12345,"ThisBarcode":"2345678901231","Price":1.00,"Measure":"EA","Description":"Some Description"},'+
'{"ThisElement":2,"Thiscode":21345,"ThisBarcode":"3124567890123","Price":2.00,"Measure":"EA","Description":"Some Description"},'+
'{"ThisElement":3,"Thiscode":31345,"ThisBarcode":"6123457890123","Price":3.00,"Measure":"EA","Description":"Some Description"},'+
'{"ThisElement":4,"Thiscode":41345,"ThisBarcode":"9123456780123","Price":4.00,"Measure":"EA","Description":"Some Description"},'+
'{"ThisElement":5,"Thiscode":51345,"ThisBarcode":"8234567901235","Price":5.00,"Measure":"EA","Description":"Some Description"}]';
ThisProductJSON:=S;
try
try
MultiObj.AddPair('ThisName','ThisValue');
MultiObj.AddPair('AnotherName','AnotherValue');
I:=0;
JSArr:=TJSONObject.ParseJSONValue(ThisProductJSON).AsType<TJSONARRAY>;
//Process all products in the array with a call to detailed Rest for each product
begin
for I := 0 to jsArr.count-1 do //iterate through the array
begin
Stockcode:= jsArr.Items[I].GetValue<string>('Thiscode');
ThisObject:=TJSONObject(jsArr[i]); // Make ThisObject Point to the jsArr[i] as an object so I can add the Other Values to this array element
OtherValues :=(GetOtherDetails(Stockcode)); // Call the below function to return the JSONVALUE containing addtional data
ThisObject.AddPair('OtherDetails',OtherValues); // Add all of the additional data in OtherValues JSONVALUE to this element of array via the object
end;
MultiObj.AddPair('WoWMultiProduct',JSARR); // This MultiObj hokds the consolidated data
end;
except
On E: Exception do
begin
//Errror handling here
end;
end;
finally
MultiObj.Free;
OtherValues.Free; // <<- I thought this would free the Result of the function but I think its leaking
JSARR.Free;
if assigned(ThisObject) then ThisObject.Free;
end;
end;
function TForm1.GetOtherDetails(Stockcode: String): TJSONVALUE;
Var
ThisResponseObject, MYRESULT : TJSONOBJECT;
vNIP, vCOO : TJSONVALUE;
begin
DetailedRESTRequest.execute; //<-- This REST Service Returns aditional JSON data
MyResult:=TJSONObject.Create;
if DetailedRESTResponse.StatusCode=200 then
begin
Try
Try
ThisResponseObject := TJSONObject.ParseJSONValue(DetailedRESTResponse.content)as TJSONObject; // Resd the entire JSON Response into TJSONObject
vCOO:=ThisResponseObject.Getvalue('COO'); // Get First JSON Value I need
vNIP:=ThisResponseObject.Getvalue('Nip'); // Get Next JSON Value I Need
MyResult.AddPair('NIPInfo',vNip); // Thiis is the only way I know how to Add the Values
MyResult.AddPair('COO',vCOO); // So that I can get them both together in the Result
Result:= TJSONObject.ParseJSONValue(MyResult.ToJSON)as TJSONValue; // Get back the JSONValue from the function
Except
On E:Exception do
begin
// Some Error Handling Here e.Classname+' ' +E.Message ;
end;
End;
Finally
If Assigned(ThisResponseObject) then ThisResponseObject.Free;
// NOTE IF I FREE MYRESULT OBJECT I GET ACCESS VIOLATION WHEN I TRY TO USE THE RESULT OF THE FUNCTION
End;
end;
end;
JSON objects are a tree-like data structure. When you request a value from a node, with getvalue (for example), it is actually giving you a reference to that node's object.
so, when you do the following in the "GetOtherDetails" function:
vCOO:=ThisResponseObject.Getvalue('COO');
MyResult.AddPair('COO',vCOO);
You make ThisResponseObject and MyResult share nodes (memory locations), so when you free one of them the other will try to free memory locations that no longer exist and generate the access violation
ThisResponseObject.free;
MyResult.Free; //access violation
Similarly, on "Function1" when doing:
ThisObject:=TJSONObject(jsArr[i]);
OtherValues :=(GetOtherDetails(Stockcode));
ThisObject.AddPair('OtherDetails',OtherValues);
You're making ThisObject contain OtherValues object... so when you try to free the two objects you're going to run into memory problems.
As the title suggest I am trying to extract a value from a very simply structured JSON file using Delphi 7 and the SuperObject Library, but I have yet to find any examples that cover this most basic topic and was hoping some of the gurus here might be able to offer me some assistance.
What I have is a simple JSON file (named test.json) that has the following structure and what I am wanting to know is how can I load this file in delphi and then extract the value for "last name" from the information provided.
I am sure this is an extremely simple task, but as I stated before I was not able to find any examples on how to do this and was hoping for some help.
example JSON file
{
id: 212,
first_name: "bob",
last_name: "smith",
age: 25
}
First, declare an instance of the object, as an ISuperObject interface in this case. Then, assign it using TSuperObject.ParseString or even just SO to parse your JSON string. Then, you can read the values using the one-letter properties, depending on the type of value you're reading...
var
O: ISuperObject;
ID, Age: Integer;
FirstName, LastName: String;
begin
O:= SO(MyJsonString);
ID:= O.I['id'];
FirstName:= O.S['first_name'];
LastName:= O.S['last_name'];
Age:= O.I['age'];
end;
Please bear in mind however that things don't typically work this way here at Stack Overflow. The only reason I answered is because it was quick and easy, and because you appear to be new here. There are plenty of resources out there on how to use SuperObject; in the demos that you downloaded with the library, all over Google, and right here in Stack Overflow.
The next is my example
function GetLastName(const FileName: string): string;
var
O: ISuperObject;
begin
// transport json file to superobject;
O:= TSuperObject.ParseFile(FileName, False);
// get value of object memeber
result:= O['last_name'].AsString;
end;
Good evening all.
I'm currently developing a cross-platform compatible version of my product WinFlare. The issue I'm facing is that SuperObject still isn't cross-platform compatible with Firemonkey. By all means, I used it in the original version of the product, but now I want to create a cross-platform version as opposed to one limited to just Windows, I'm finding it to be a hassle.
DBXJSON is the only cross-platform solution I've been able to find after extensive hours of research, but that's proving to be frustrating to try and deal with. Most all of the examples I've found for it either don't apply for my situation, or they're too complicated to gleam anything useful from. There's lots of discussion, but I'm just struggling to get to grips with what was such a simple task with SuperObject. I've spent the best part of this evening trying to find something that works to build from, but everything I've tried has just led me back to square one.
Ideally, I'd like to fix up SuperObject, but I lack the knowledge to go so in depth as to make it cross-platform compatible with OS X (and ready for the mobile studio). I'd welcome any suggestions on that, but as I imagine no one's got the time to go through such a huge task, it looks like DBXJSON is my only option.
The JSON layout I'm dealing with is still the same;
{
response: {
ips: [
{
ip: "xxx.xxx.xxx.xxx",
classification: "threat",
hits: xx,
latitude: xx,
longitude: xx,
zone_name: "domain-example1"
},
{
ip: "yyy.yyy.yyy.yyy",
classification: "robot",
hits: yy,
latitude: xx,
longitude: xx,
zone_name: "domain-example2"
}
]
}
result : "success",
msg: null
}
There can be hundreds of results in the ips array. Let's say I want to parse through all of the items in the array and extract every latitude value. Let's also assume for a second, I'm intending to output them to an array. Here's the sort of code template I'd like to use;
procedure ParseJsonArray_Latitude(SInput : String);
var
i : Integer;
JsonArray : TJsonArray;
Begin
// SInput is the retrieved JSON in string format
{ Extract Objects from array }
for i := 0 to JsonArray.Size-1 do
begin
Array_Latitude[i] := JsonArray.Item[i].ToString;
end;
end;
Essentially, where it says { Extract Objects from array }, I'd like the most basic solution using DBXJSON that would solve my problem. Obviously, the calls I've shown related to JsonArray in the template above might not be correct - they're merely there to serve as an aid.
First, parse the string to get an object.
var
obj: TJsonObject;
obj := TJsonObject.ParseJsonValue(SInput) as TJsonObject;
That gives you an object with three attributes, response, result, and msg. Although ParseJsonValue is a method of TJsonObject, and your particular string input happens to represent an object value, it can return instances of any TJsonValue descendant depending on what JSON text it's given. Knowing that's where to start is probably the hardest part of working with DbxJson.
Next, get the response attribute value.
response := obj.Get('response').JsonValue as TJsonObject;
That result should be another object, this time with one attribute, ips. Get that attribute, which should have an array for a value.
ips := response.Get('ips').JsonValue as TJsonArray;
Finally, you can get the values from the array. It looks like you're expecting the values to be numbers, so you can cast them that way.
for i := 0 to Pred(ips.Size) do
Array_Latitude[i] := (ips.Get(i) as TJsonObject).Get('latitude').JsonValue as TJsonNumber;
Remember to free obj, but not the other variables mentioned here, when you're finished.
For completion, since the question stated that there was no alternative to DBXJSON for cross-platform, I would like to point out two Open Source alternatives, which appeared since the initial question.
XSuperObject has an API very close to SuperObject, but is cross-platform;
Our SynCrossPlatformJSON.pas unit, which is lighter and much faster than both DBXJSON and XSuperObject.
SynCrossPlatformJSON is able to create schema-less objects or arrays, serialize and unserialize them as JSON, via a custom variant type, including late-binding to access the properties.
For your problem, you could write:
var doc: variant;
ips: PJSONVariantData; // direct access to the array
i: integer;
...
doc := JSONVariant(SInput); // parse JSON Input and fill doc custom variant type
if doc.response.result='Success' then // easy late-binding access
begin
ips := JSONVariantData(doc.response.ips); // late-binding access into array
SetLength(Arr_Lat,ips.Count);
for i := 0 to ips.Count-1 do begin
Arr_lat[i] := ips.Values[i].latitude;
Memo1.Lines.add(ips.Values[i].latitude);
end;
end;
... // (nothing to free, since we are using variants for storage)
Late-binding and variant storage allow pretty readable code.
Thanks to assistance from Rob Kennedy, I managed to build a solution that solved the problem;
var
obj, response, arrayobj : TJSONObject;
ips : TJSONArray;
JResult : TJsonValue;
i : Integer;
Arr_Lat : Array of string;
begin
try
Memo1.Lines.Clear;
obj := TJsonObject.ParseJSONValue(SInput) as TJSONObject;
response := Obj.Get('response').JsonValue as TJSONObject;
ips := response.Get('ips').JsonValue as TJSONArray;
SetLength(Arr_Lat, ips.Size-1);
for i := 0 to ips.Size-1 do
begin
arrayobj := ips.Get(i) as TJSONObject;
JResult := arrayobj.Get('latitude').JsonValue;
Arr_lat[i] := JResult.Value;
Memo1.Lines.Add(JResult.Value);
end;
finally
obj.Free;
end;
This will add the results to both the array (Arr_Lat), and output them to the memo (Memo1).
Good evening guys!
I'm currently trying to put together a CloudFlare client for the desktop. I've connected to their API and successfully retrieved the JSON results with a POST request (the results of which have been output into a TMemo). I'm now wanting to parse these results into a TListBox (see bolded area for example). The project is being designed in Firemonkey.
Here's the formatted layout of the response with some example content;
{
- response: {
|- ips: [
|- {
ip: "xxx.xxx.xxx.xxx",
classification: "threat",
hits: xx,
latitude: null,
longitude: null,
zone_name: "domain-example1"
},
- {
ip: "yyy.yyy.yyy.yyy",
classification: "robot",
hits: yy,
latitude: null,
longitude: null,
zone_name: "domain-example2"
}
]
}
result : "success",
msg: null
}
I've tried several different components - SuperObject, Paweł Głowacki's JSON Designtime Parser, Tiny-JSON, LKJSON and the built in DBXJSON. However, i've no experience with JSON at all and i can't seem to find the most basic of examples that i can get started from. Many of them show sample data, but all the ones i've tried don't seem to work as i'd expect, most likely because i'm misunderstanding them. I'd assume the components work, so i need guidance on getting started.
There are hundreds, often thousands, of results in the ips "array" (i apologise if that's not correct, i'd assume it's known as an array but again, i'm completely new to JSON).
What i'm really looking for is some sort of extremely basic sample code which i can build from (along with what component it uses for parsing and such).
For example, if i wanted to grab every ip from the JSON results, and put each one as a separate item into a TListBox (using TListBox.add method), how would i go about achieving this?
When i say ip, i mean the value (in the formatted layout above, this would be xxx.xxx.xxx.xxx or yyy.yyy.yyy.yyy).
Additionally, if i wanted to find a "record" (?) by it's IP from the JSON results and output the data to a delphi array - e.g.;
Result : Array of String = ['"xxx.xxx.xxx.xxx"','"threat"','xx','null','null','"domain-example1"'];
is that possible with JSON? (If this is seen as a separate question or too unrelated, please feel free to edit it out rather than close the question as a whole).
The closest i got to this had not only the ip's, but every other piece of data in a seperate TListItem (i.e. response, ips, ip, classification, xxx.xxx.xxx.xxx and everything else had it's own item, along with several empty items in between each non-empty item).
I'm sure it's extremely simple to do, but there's so much information on JSON that it's a little overwhelming for people new to the format.
Best Regards,
Scott Pritchard.
JSON is very simple and easy to figure out, once you understand the basic concepts. Have a look at http://json.org, where it explains things.
There are 4 basic concepts in JSON:
A value is any JSON element: a basic string or number, an array, or an object. (Anything but a pair.)
An array should be a familiar concept: an ordered list of values. The main difference from Delphi arrays is that JSON arrays don't have a defined type for the elements; they're simply "an array of JSON values."
A pair is a key-value pair. The key can be a string or a number, and the value can be any JSON value.
An object is an associative map of JSON pairs. You can think of it conceptually as a TDictionary<string, JSON value>.
So if I wanted to take a JSON array of data like that, and put it in a TListBox, I'd do something like this (DBXJSON example, warning: not tested):
procedure TMyForm.LoadListBox(response: TJSONObject);
var
i: integer;
ips: TJSONArray;
ip: TJSONObject;
pair: TJSONPair;
begin
ListBox.Clear;
pair := response.Get('ips');
if pair = nil then
Exit;
ips := pair.value as TJSONArray;
for i := 0 to ips.size - 1 do
begin
ip := ips.Get(i) as TJSONObject;
pair := ip.Get('ip');
if pair = nil then
ListBox.AddItem('???', ip.Clone)
else ListBox.AddItem(pair.JsonString, ip.Clone);
end;
end;
Then you have a list of IP addresses, and associated objects containing the full record that you can get at if the user selects one. (If you wanted to put the entire contents of each record into the list control, have a look at TListView. It works better than TListBox for that.)
And if you want to build an array of strings containing all the values, do something like this:
function JsonObjToStringArray(obj: TJsonObject): TArray<string>;
var
i: integer;
begin
SetLength(result, obj.Size);
for i := 0 to obj.Size - 1 do
result[i] := obj.Get(i).JsonValue.ToString;
end;
This is all just sample code, of course, but it should give you something to build on.
EDIT2: AV Fixed with extreme ease.
EDIT: After further examining my own code, i realised it would cause a massive amount of memory leaks. However, i have since switched over to SuperObject and found the same result can be achieved in 2 lines of code with only 2 variables and no memory leaks;
Procedure ParseIPs;
ISO : ISuperObject;
MyItem : ISuperObject;
begin
ISO := SO(RetrievedJSON);
for MyItem in ISO['response.ips'] do Memo2.Lines.Add(MyItem.S['ip']);
end;
RetrievedJSON is simply a string containing the unparsed, plaintext JSON (i.e. not a JSONString but an actual string).
I've left the original code underneath for sake of continuity.
With assistance from Mason Wheeler in an earlier answer, as well as an answer provided by "teran" on question 9608794, i successfully built the following to parse down to the actual level (i.e. the "array" containing the data) i needed to access, and then output all items with a specific JSONString.Value into a listbox (named LB1 in the sample below);
Procedure ParseIP;
var
o, Jso, OriginalObject : TJSONObject;
ThePair, JsPair : TJSONPair;
TheVal, jsv : TJSONValue;
jsArr : TJsonArray;
StrL1 : String;
i, num : Integer;
begin
num := 0;
o := TJSONObject.ParseJSONValue(TEncoding.ASCII.GetBytes(Memo1.Text), 0) as TJSONObject;
ThePair := o.Get('response');
TheVal := ThePair.JsonValue;
STRL1 := TheVal.ToString;
JSV := TJSONObject.ParseJSONValue(STRL1);
OriginalObject := JSV as TJSONObject;
JSPair := OriginalObject.Get('ips');
JSARR := JSPair.JsonValue as TJSONArray;
for i := 0 to JsArr.Size-1 do
begin
JSO := JSArr.Get(i) as TJSONObject;
for JSPAIR in JSO do
begin
num := num+1;
if JSPAIR.JsonString.Value = 'ip' then
begin
LB1.Items.Add(JSPair.JsonValue.Value);
end
else null;
end;
end;
ShowMessage('Items in listbox: ' + IntToStr(LB1.Items.Count));
ShowMessage('Items in JSON: ' + IntToStr(num div JSO.Size));
Jsv.Free;
end;
While this is an extremely round-about way of doing it, it allows me to look at each individual step, and see where it's iterating down through the JSON and with extreme ease, and to change it into a function where i can output any piece or range of data as a result based on one of multiple criteria. For the sake of verifying i got the correct number of items, i added 2 ShowMessage routines at the end; One for the items in the listbox, and one for the number of instances of "ip" data that i was parsing.
This code was specifically tested in Firemonkey with CloudFlare API JSON results which were output into a TMemo exactly as they were retrieved (on an &calls_left&a=zone_ips&class=t&geo=1 API call, of course with your zone, token and email appended in addition). It should be relatively easy to modify it to work with other results from the numerous other API calls too.
To clarify, i did try Mason's code, but unfortunately i couldn't get it working. However, i have accepted his answer for the time being on the basis that the explanation he gave on the basics was worthy of it and assisted me in getting to an end-solution and coming up with something i can build from and teach myself.