Parsing JSON with Dart - json

I want to be able to parse a string to an object that I can access using the dot notation e.g. myobject.property, instead of the array notation e.g. myobject['property']. The array notation works fine. Here's what I have so far.
I have some XML:
<level1 name="level1name">
<level2 type="level2Type">
<entry>level2entry</entry>
<entry>level2entry</entry>
</level2>
</level1>
Which converts to the JSON:
{
"level1": {
"name": "level1name",
"level2": {
"type": "level2Type",
"entry": [
"level2entry",
"level2entry"
]
}
}
}
I have the following Dart code:
Object jsonObject = JSON.parse("""{
"level1": {
"name": "level1name",
"level2": {
"type": "level2Type",
"entry": [
"level2entry",
"level2entry"
]
}
}
}
""");
print("my test 1 == ${jsonObject}");
print("my test 2 == ${jsonObject['level1']}");
print("my test 3 == ${jsonObject['level1']['name']}");
which produce the (desired) output:
my test 1 == {level1: {name: level1name, level2: {type: level2Type, entry: [level2entry, level2entry]}}}
my test 2 == {name: level1name, level2: {type: level2Type, entry: [level2entry, level2entry]}}
my test 3 == level1name
But when I try:
print("my test 1 == ${jsonObject.level1}");
I get the following:
Exception: NoSuchMethodException : method not found: 'get:level1'
Receiver: {level1: {name: level1name, level2: {type: level2Type, entry: [level2entry, level2entry]}}}
Arguments: []
Stack Trace: 0. Function: 'Object.noSuchMethod' url: 'bootstrap' line:717 col:3
Ideally, I want an object that I can access using the dot notation and without the compiler giving warning about Object not having property. I tried the following:
class MyJSONObject extends Object{
Level1 _level1;
Level1 get level1() => _level1;
set level1(Level1 s) => _level1 = s;
}
class Level1 {
String _name;
String get name() => _name;
set name(String s) => _name = s;
}
...
MyJSONObject jsonObject = JSON.parse("""{
"level1": {
"name": "level1name",
"level2": {
"type": "level2Type",
"entry": [
"level2entry",
"level2entry"
]
}
}
}
""");
...
print("my test 1 == ${jsonObject.level1.name}");
but instead of giving me 'level1name' as hoped, I get:
Exception: type 'LinkedHashMapImplementation<String, Dynamic>' is not a subtype of type 'MyJSONObject' of 'jsonObject'.
What am I doing wrong here? Is there any way to do what I'm trying? Thanks.

At the moment, JSON.parse only returns Lists (array), Maps, String, num, bool, and null
(api ref).
I suspect that until reflection makes it way into the language, it won't be able to re-construct objects based upon the keys found in json.
You could, however, create a constructor in your MyJsonObject which took a string, called JSON.parse internally, and assigned the various values.
Something like this works in the dart editor:
#import("dart:json");
class Level2 {
var type;
var entry;
}
class Level1 {
var name;
var level2;
}
class MyJSONObject {
Level1 level1;
MyJSONObject(jsonString) {
Map map = JSON.parse(jsonString);
this.level1 = new Level1();
level1.name = map['level1']['name'];
level1.level2 = new Level2();
level1.level2.type = map['level1']['level2']['type'];
//etc...
}
}
main() {
var obj = new MyJSONObject(json);
print(obj.level1.level2.type);
}
A non trivial version would needs some loops and possible recursion if you had deeper nested levels.
Update: I've hacked together a non-trivial version (inspired by the post below), it's up on github (also taking Seth's comments re the constructor):

Chris is completely right. I will only add that the JSON parser could be modified to return a little richer object (something like JsonMap instead of pure Map) that could allow jsonObj.property by implementing noSuchMethod. That would obviously perform worse than jsonObj['property'].

Related

Add item to JSON array using JsonPatchDocument

I am trying to add an element to a JSON array using Microsoft's JsonPatch implementation in .NET 6:
JSON input:
{ "foo": [ 1 ] }
Expected JSON output:
{ "foo": [ 1, 2 ] }
Following their documentation, I ended up with the following code:
string input = #"{ ""foo"": [ 1 ] }";
dynamic obj = JsonSerializer.Deserialize<ExpandoObject>(input);
var patch = new JsonPatchDocument();
patch.Add("/foo/-", 2);
string output = JsonSerializer.Serialize(obj);
Console.WriteLine(output); // throws JsonPatchException, expected { "foo": [ 1, 2 ] }
I expect the foo property of my object to contain an array equal to [1, 2], but instead it fails with the following error:
Microsoft.AspNetCore.JsonPatch.Exceptions.JsonPatchException: The target location specified by path segment '-' was not found.
A Replace operation on the foo property successfully updates the ExpandoObject, but the Add operation fails. Am I missing something obvious?
I also tried using JsonNode instead of ExpandoObject to no avail (JsonNode obj = JsonSerializer.Deserialize<JsonNode>(input);). The code throws the same error.
In the meantime, as a workaround, I am using JsonPatch.Net. The code looks similar:
string input = #"{ ""foo"": [ 1 ] }";
JsonNode obj = JsonSerializer.Deserialize<JsonNode>(input);
var patch = new JsonPatch(PatchOperation.Add(JsonPointer.Parse("/foo/-"), 2));
PatchResult patchResult = patch.Apply(obj);
string output = JsonSerializer.Serialize(patchResult.Result);
Console.WriteLine(output); // { "foo": [ 1, 2 ] }

Net core dapper and postgres jsonb column

I want to POST some custom JSON to my postgres jsonb column via postman using the below request.
The custom part is sent in the "Settings" > "data" node. I don't want to apply the custom part to a model I just want to send in any kind of json and store it.
{
"name": "Test",
"settings": {
"data": {
"customdata": "hello",
"custommore": "bye"
}
}
}
The "data" node is modelled - like this:
public string Data { get; set; } //I have tried JSONDocument and Jsonb types to no avail.
Postman errors with this:
"errors": {
"$.settings.data": [
"The JSON value could not be converted to System.String. Path: $.settings.data | LineNumber: 3 | BytePositionInLine: 17."
]
}
The request doesn't even hit my controller method. I think it is because the customdata and custommore is not mapped to a model.
Is there a way of sending in custom JSON data that is not fixed to a model of any kind - or must it be part of a model?
I'm struggling to find anything about this that doesn't relate to EF core which is not what I am using.
You can use custom model binding,and get json data from HttpContext.Request.Body,and then use sonConvert.DeserializeObject to get json object.You can set the data to the format you want.
Here is a demo:
DataBinder:
public class DataBinder:IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var model1 = new Customer();
using (var reader = new StreamReader(bindingContext.HttpContext.Request.Body))
{
var body = reader.ReadToEndAsync();
var mydata = JsonConvert.DeserializeObject<JObject>(body.Result);
model1.Name = mydata["name"].ToString();
model1.Settings = new Settings
{
Data = mydata["settings"]["data"].ToString()
};
}
bindingContext.Result = ModelBindingResult.Success(model1);
return Task.CompletedTask;
}
}
Controller:
public IActionResult TestCustomModelBinding([ModelBinder(BinderType = typeof(DataBinder))]Customer customer) {
return Ok();
}
result:

Tornadofx REST client

I have followed an example shown here
link
And i got the hang of it, i managed to create my own "Employee" entity and i found some dummy api data online to play with.
like this Problem is, the tornadofx throws null pointer error, and i think its because the rest response sends something like this
{
"status": "success",
"data": [
{
"id": "1",
"employee_name": "Tiger Nixon",
"employee_salary": "320800",
"employee_age": "61",
"profile_image": ""
},
but when i use mocky and provide JUST the json part
[
{
"id": "1",
"employee_name": "Tiger Nixon",
"employee_salary": "320800",
"employee_age": "61",
"profile_image": ""
},...]
it all works fine.
I think those additional fields "status" and "success" in response confuse the rest client of tornadofx, and i cant manage to get it to work, is there anyway to tell client to ignore every other fields besides those of json data.
All links are functional, so you can try yourself.
full working example
package com.example.demo.view
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleStringProperty
import javafx.scene.layout.BorderPane
import tornadofx.*
import javax.json.JsonObject
class Employee (id:Int?=null , name: String? = null, age: Int?=null): JsonModel {
val idProperty = SimpleIntegerProperty(this, "id")
var id by idProperty
val ageProperty = SimpleIntegerProperty(this, "age")
var age by ageProperty
val employeeNameProperty = SimpleStringProperty(this, "name", name)
var name by employeeNameProperty
override fun updateModel(json: JsonObject) {
with(json) {
id = int("id")!!
age = int("employee_age")!!
name = string("employee_name")
}
}
override fun toJSON(json: JsonBuilder) {
with(json) {
add("id", id)
add("employee_name", name)
add("employee_age", age)
}
}
}
class PersonEditor : View("Person Editor") {
override val root = BorderPane()
val api : Rest by inject()
var persons = listOf(Employee(1,"John", 44), Employee(2,"Jay", 33)).observable()
val model = PersonModel(Employee())
init {
api.baseURI = "https://run.mocky.io/v3/"
val response = api.get("f17509ba-2d12-4c56-b441-69ab23302e43")
println(response.list())
println(response.list().toModel<Employee>()[0].name)
// print( p.get(1))
with(root) {
center {
tableview(response.list().toModel<Employee>()) {
column("Id", Employee::idProperty)
column("Name", Employee::employeeNameProperty)
column("Age", Employee::ageProperty)
// Update the person inside the view model on selection change
model.rebindOnChange(this) { selectedPerson ->
item = selectedPerson ?: Employee()
}
}
}
right {
form {
fieldset("Edit person") {
field("Id") {
textfield(model.id)
}
field("Name") {
textfield(model.name)
}
field("Age") {
textfield(model.age)
}
button("Save") {
enableWhen(model.dirty)
action {
save()
}
}
button("Reset").action {
model.rollback()
}
}
}
}
}
}
private fun save() {
// Flush changes from the text fields into the model
model.commit()
// The edited person is contained in the model
val person = model.item
// A real application would persist the person here
println("Saving ${person.employeeNameProperty} / ${person.ageProperty}")
}
}
class PersonModel(person: Employee) : ItemViewModel<Employee>(person) {
val id = bind(Employee::idProperty)
val name = bind(Employee::employeeNameProperty)
val age = bind(Employee::ageProperty)
}
if you replace base url and send request to http://dummy.restapiexample.com/api/v1/employees you will get an error that i am talking about
Your call to mocky returns a list, so .list() works fine. Your call to restapiexample, however, returns an object, not a list, so .list() won't do what you expect. You can probably use something like this, though I haven't tested it:
response.one().getJsonArray("data").toModel<Employee>()[0].name)
Further explanation:
If you're not familiar with the structure of JSON, check out the diagrams on the JSON homepage.
TornadoFX has two convenience functions for working with JSON returns: .list() and .one(). The .list() function will check if the result is a JsonArray. If so, it simply returns it. If it is instead a JsonObject, it wraps that object in a list and returns the new list.
In your case, since restapiexample is returning an object, the result of your call to .list() is a JsonArray with a single object. It looks something like this:
[
{
"status": "success",
"data": [...]
}
]
Obviously that single object cannot be converted to an Employee, so dereferencing anything off of it will result in a NullPointerException.
The .one() function on the other hand will check if the response is a JsonObject. If it is, it simply returns the object. If, however, the response is a JsonArray, it will take the first item from the array and return that item.

Getting element value from jsonpath whose root is an array

I have a JSON response which has root as an array of 1 or more objects. I want to extract the value of one of the elements within each object.
Here is the JSON sample:
[
{
"od_pair":"7015400:8727100",
"buckets":[
{
"bucket":"C00",
"original":2,
"available":2
},
{
"bucket":"A01",
"original":76,
"available":0
},
{
"bucket":"B01",
"original":672,
"available":480
}
]
},
{
"od_pair":"7015400:8814001",
"buckets":[
{
"bucket":"C00",
"original":2,
"available":2
},
{
"bucket":"A01",
"original":40,
"available":40
},
{
"bucket":"B01",
"original":672,
"available":672
},
{
"bucket":"B03",
"original":632,
"available":632
},
{
"bucket":"B05",
"original":558,
"available":558
}
]
}
]
I want to access the values of od_pair within each object.
I tried referring to the root array as $ but that did not help.
This is the code snippet I have written:
List<Object> LegList = jsonPath.getList("$");
int NoofLegs = LegList.size();
System.out.println("No of legs :" +NoofLegs);
for (int j=0; j<=NoofLegs;j++) {
String OD_Pair = jsonPath.param("j", j).getString("[j].od_pair");
System.out.println("OD Pair: " + OD_Pair);
List<Object> BucketsList = jsonPath.param("j", j).getList("[j].buckets");
int NoOfBuckets = BucketsList.size();
System.out.println("no of Buckets: " + NoOfBuckets);
}
This is the error that I see:
Caused by:
org.codehaus.groovy.control.MultipleCompilationErrorsException: startup
failed:
Script1.groovy: 1: unexpected token: [ # line 1, column 27.
restAssuredJsonRootObject.[j].od_pair
Can someone kindly help me here please?
You were right to start with the $. However, What you get with your particular JSON is List of HashMap<String, Object> where each JSON Object is represented as a single HashMap. Knowing that you can obtain the list of HashMaps like this:
List<HashMap<String, Object>> jsonObjectsInArray = path.getList("$");
The String will be the name of the attribute. The Object will be either String, Integer, JSONObject or JSONArray. The latter isn't exact class names but it's not relevant to you to achieve desired results.
Now, all we have to do is iterate over the HashMap and extract values of od_pair like this:
for (HashMap<String, Object> jsonObject : jsonObjectsInArray) {
System.out.println(jsonObject.get("od_pair"));
}
The output is:
7015400:8727100
7015400:8814001
Hope it helps!

Combine JSON and String in a dictionary with Swifty

I'd like to create a JSON object in Swifty that has the form:
{
"store": {
"id": {
"test": "test"
},
"type": "retail",
"name": "store1"
}
}
Is there a way to combine types in a Dictionary to use with Swifty (String and JSON)? Quotes works, but when I try to assign a variable, it complains: Cannot assign value of type 'String' to type 'JSON?':
func jsonTest()->String {
var storeJson = [String: JSON]()
var someJson = JSON(["test":"test"])
storeJson["id"] = someJson
storeJson["type"] = "retail" // <-- works fine
var name = "store1"
storeJson["name"] = name // <-- Doesn't work
var store = JSON(storeJson)
return store.rawString()!
}
The reason
storeJson["type"] = "retail"
works differently than
storeJson["name"] = name
is because the first one follows a different path in the code. Specifically, it uses the init(stringLiteral value: StringLiteralType) method in the following extension (source).
extension JSON: Swift.StringLiteralConvertible {
public init(stringLiteral value: StringLiteralType) {
self.init(value)
}
public init(extendedGraphemeClusterLiteral value: StringLiteralType) {
self.init(value)
}
public init(unicodeScalarLiteral value: StringLiteralType) {
self.init(value)
}
}
I'll explain further after we talk about how to fix your specific problem.
Possible solution #1:
storeJson["name"]?.string = name
Output:
{
"id" : {
"test" : "test"
},
"type" : "retail"
}
The reason
storeJson["name"]?.string = name
doesn't work as we might think is because of the optional chaining. Right now, if we ran this through the debugger, we wouldn't see anything meaningful. In fact, we would see nothing. This is a bit concerning and likely means storeJson["name"] is nil, so the statement is not executing any further. Let's verify our hypothesis by making it blow up. We'll change the line to:
storeJson["name"]!.string = name
In this case, with your current code, you'll likely get
fatal error: unexpectedly found nil while unwrapping an Optional value
as you should because storeJson["name"] is in fact nil. Therefore, this solution doesn't work.
Possible solution #2:
As you correctly noted in your answer, if you add a storeJson["name"] = JSON(name), you'll get the desired behavior:
func jsonTest()->String {
var storeJson = [String: JSON]()
var someJson = JSON(["test":"test"])
storeJson["id"] = someJson
storeJson["type"] = "retail" // <-- works fine
var name = "store1"
storeJson["name"] = JSON(name) // <-- works!
var store = JSON(storeJson)
return store.rawString()!
}
Output:
{
"id" : {
"test" : "test"
},
"name" : "store1",
"type" : "retail"
}
Great! Therefore, this solution works! Now, later in your code you can alter it however you want using .string and the like.
Explanation
Back to why the string literal works. You'll notice in the init, it has
self.init(value)
which passes through the objects init, which then goes through the case statement
...
case let string as String:
_type = .String
self.rawString = string
...
When you call storeJson["name"] = JSON(name), you're skipping the StringLiteralType init and simply going into the switch.
Therefore, you could interchange
storeJson["type"] = "retail"
with
storeJson["type"] = JSON("retail")
It turns out it works to change:
storeJson["name"] = name
to
storeJson["name"] = JSON(name)