Combine JSON and String in a dictionary with Swifty - json

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)

Related

Rescript manipulate JSON file

I have this JSON file.
Using rescript I want to :
Read the file.
Extract data from the file.
Write result in a new file.
{
"name": "name",
"examples": [
{
"input": [1,2],
"result": 1
},
{
"input": [3,4],
"result": 3
}
],
}
I was able to acheive this using JavaScript
var file = Fs.readFileSync("file.json", "utf8");
var data = JSON.parse(file);
var name = data.name
var examples = data.examples
for (let i = 0; i< examples.length; i++){
let example = examples[i]
let input = example.input
let result = example.result
let finalResult = `example ${name}, ${input[0]}, ${input[1]}, ${result} \n`
Fs.appendFileSync('result.txt',finalResult)
}
These are my attempts at writing it in Rescript and the issues I ran into.
let file = Node.Fs.readFileSync("file.json", #utf8)
let data = Js.Json.parseExn(file)
let name = data.name //this doesn't work. The record field name can't be found
So I have tried a different approach (which is a little bit limited because I am specifying the type of the data that I want to extract).
#module("fs")
external readFileSync: (
~name: string,
[#utf8],
) => string = "readFileSync"
type data = {name: string, examples: array<Js_dict.t<t>>}
#scope("JSON") #val
external parseIntoMyData: string => data = "parse"
let file = readFileSync(~name="file.json", #utf8)
let parsedData = parseIntoMyData(file)
let name = parsedData.name
let example = parsedData.examples[0]
let input = parsedData.examples[0].input //this wouldn't work
Also tried to use Node.Fs.appendFileSync(...) and I get The value appendFileSync can't be found in Node.Fs
Is there another way to accomplish this?
It's not clear to me why you're using Js.Dict.t<t> for your examples, and what the t in that type refers to. You certainly could use a Js.Dict.t here, and that might make sense if the shape of the data isn't static, but then you'd have to access the data using Js.Dict.get. Seems you want to use record field access instead, and if the data structure is static you can do so if you just define the types properly. From the example you give, it looks like these type definitions should accomplish what you want:
type example {
input: (int, int), // or array<int> if it's not always two elements
result: int,
}
type data = {
name: string,
examples: array<example>,
}

Parsing JSON error cannot read property 'url' of undefined

I have been trying to parse JSON, which have 3 different set of data where one element have various number of children and sometimes none. I am getting an error when there is no children present or only one present. I declared the JSON as var data.
JSON A
{
"floorplan": [
{
"title": "plan1",
"url": "https://media.plan1.pdf"
},
{
"title": "plan2",
"url": "https://media.plan2.pdf"
}
]
}
JSON B
{"floorplan": []}
JSON C
{
"floorplan": [
{
"title": "plan1",
"url": "https://media.plan1.pdf"
}
]
}
I parsed the JSON like this:
var items = JSON.parse(data);
return {
floorplan1: items.floorplan[0].url;
floorplan2: items.floorplan[1].url;
}
But, it only returned data for the JSON A, for other 2 it gave TypeError: Cannot read property 'url' of undefined.
I modified the code to check if floorplan have at least one child and then parse data.
var items = JSON.parse(data);
var plan = items.floorplan[0];
if(plan){
return {
floorplan1: items.floorplan[0].url;
floorplan2: items.floorplan[1].url;
}
}
The new code returned data for JSON A and B(as empty row), but gave error for C. C have one child still it got the error.
I also tried this code, still got the error for JSON C.
var items = JSON.parse(data);
var plan = items.floorplan[0];
var plan1;
var plan2;
if(plan){
plan1 = items.floorplan[0].url;
plan2 = items.floorplan[1].url;
}
return{
floorplan1 : plan1 ? plan1 : null;
floorplan2 : plan2 ? plan2 : null;
}
Is there any method I can try to get data returned for all 3 types of JSON?
let data = `
[{"floorplan": [{
"title": "plan1",
"url": "https://media.plan1.pdf"
}, {
"title": "plan2",
"url": "https://media.plan2.pdf"
}]},
{"floorplan": []},
{"floorplan": [{
"title": "plan1",
"url": "https://media.plan1.pdf"
}]}]`;
let json = JSON.parse(data);
//console.log(json);
json.forEach(items=>{
//console.log(items);
let o = {
floorplan1: items.floorplan.length > 0 ? items.floorplan[0].url : '',
floorplan2: items.floorplan.length > 1 ? items.floorplan[1].url : ''
};
console.log(o);
o = {
floorplan1: (items.floorplan[0] || {'url':''}).url,
floorplan2: (items.floorplan[1] || {'url':''}).url
};
console.log(o);
o = {
floorplan1: items.floorplan[0]?.url,
floorplan2: items.floorplan[1]?.url
};
console.log(o);
const {floorplan: [one = {url:''}, two = {url:''}]} = items;
o = {
floorplan1: one.url,
floorplan2: two.url
};
console.log(o);
});
Sure. A few ways, and more than I have here. I have put all the raw data into one string, parsed it into json and then iterated through that. In each loop my variable items will correspond to one of the json variables you created and referenced in your question as items.
In the first example, I check to make sure that items.floorplan has at least enough elements to contain the url I'm trying to reference, then use the ternary operator ? to output that URL if it exists or an empty string if it doesn't.
In the second example, I use the || (OR) operator to return the first object that evaluates to true. If items.floorplan[x] exists, then it will be that node, and if it doesn't I provide a default object with an empty url property on the right hand side, and then just use the url from the resulting object.
In the third, I use the optional chaining operator that was introduced in 2020. This method will return undefined if the url doesn't exist.
In the fourth example, I use destructuring to pull values out of the items variable, and make sure that there is a default value for url in case the items variable doesn't have a corresponding value.
But there are many more ways to go about it. These are just a few, and you can't necessarily say which approach is better. It's dependent on your intent and environment. With the exception of optional chaining (which shows undefined if the property doesn't exist), you can see these produce the same results.
DOCS for optional chaining: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining
DOCS for destructuring: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment
An article on destructuring: https://javascript.info/destructuring-assignment

Add new attribute to JSON

Using Node js and Sequelize ORM, i'm getting a data set. I need to add a new attribute to received data and send it to client side. This is what i tried.
Code Block 1
var varAddOns = { "id" : 5, "Name" : "Cheese"};
global.meal.findOne(
{
where: { id: 5 }
}).then(varMeal => {
var obj = {};
obj = varMeal;
obj.addons = varAddOns;
res.send(obj);
});
It returns a json like below. (Actually it does not contain "addons" data)
Code Block 2
{
"id": 12,
"mealName": "Burger",
"description": "Oily food",
}
but actually what i want is,
Code Block 3
{
"id": 12,
"mealName": "Burger",
"description": "Oily food",
"addons" : {
"id" : 5,
"Name" : "Cheese"
}
}
I tried something like below and it also wont work. (It returns same json as "Code Block 2'.)
Code Block 4
var newJson = {};
newJson = JSON.stringify(varMeal);
newJson['addons'] = varAddOns;
var retVal = JSON.parse(newJson);
res.send(retVal);
Can you help me to figure out, where the issue is?
EDIT
Code Block 5
var newJson = {};
newJson = varMeal;
newJson['addons'] = varAddOn;
var retVal = newJson;// JSON.parse(newJson);
res.send(retVal);
I tried 'Code block 5' as well. Same result comes out as 'Code block 2'. When I use JSON.parse(newJson), it was thrown an error. (Error is Unexpected token o in JSON at position 1)
You need to call .get on your model instance, and then attach extra properties to it:
var varAddOns = { "id" : 5, "Name" : "Cheese"};
global.meal.findOne(
{
where: { id: 5 }
}).then(varMeal => {
var obj = {};
obj = varMeal.get();
obj.addons = varAddOns;
res.send(obj);
});
A few things:
When you call findOne, Sequelize return a model instance, not a plain JS object with your data.
If you want to add extra properties to send to your user, you will first need to convert your model instance to a JS object with your data. You can do this by calling varMeal.get(). From there, you can add extra properties to it.
There is no need to prepend your variables with "var". It would be better to simply name your variable meal
you need the JSON to be an object when you are declaring newJson['addons'] as a nested object
Have you tried (in code block 4) not stringifying varMeal?

ServiceStack.Text.JsonObject.Parse vs. NewtonSoft.Json.Linq.JObject.Parse for nested tree of 'dynamic' instances?

I'd like to try ServiceStack's json parsing, but I've already figured out how to do something I need via Newtonsoft. Can this same thing by done via ServiceStack?
I've tried with the commented out code but it gives exceptions, see below for exception details.
Thanks!
Josh
[Test]
public void TranslateFromGitHubToCommitMessage()
{
const string json =
#"
{
'commits':
[
{
'author': {
'email': 'dev#null.org',
'name': 'The Null Developer'
},
'message': 'okay i give in'
},
{
'author': {
'email': 'author#github.com',
'name': 'Doc U. Mentation'
},
'message': 'Updating the docs, that\'s my job'
},
{
'author': {
'email': 'author#github.com',
'name': 'Doc U. Mentation'
},
'message': 'Oops, typos'
}
]
}
";
dynamic root = JObject.Parse(json);
//dynamic root = ServiceStack.Text.JsonSerializer.DeserializeFromString<JsonObject>(json);
//dynamic root = ServiceStack.Text.JsonObject.Parse(json);
var summaries = new List<string>();
foreach (var commit in root.commits)
{
var author = commit.author;
var message = commit.message;
summaries.Add(string.Format("{0} <{1}>: {2}", author.name, author.email, message));
}
const string expected1 = "The Null Developer <dev#null.org>: okay i give in";
const string expected2 = "Doc U. Mentation <author#github.com>: Updating the docs, that's my job";
const string expected3 = "Doc U. Mentation <author#github.com>: Oops, typos";
Assert.AreEqual(3, summaries.Count);
Assert.AreEqual(expected1, summaries[0]);
Assert.AreEqual(expected2, summaries[1]);
Assert.AreEqual(expected3, summaries[2]);
}
Exceptions Detail
When using the first commented out line:
dynamic root = ServiceStack.Text.JsonSerializer.DeserializeFromString<JsonObject>(json);
This exception occurs when the method is called.
NullReferenceException:
at ServiceStack.Text.Common.DeserializeListWithElements`2.ParseGenericList(String value, Type createListType, ParseStringDelegate parseFn)
at ServiceStack.Text.Common.DeserializeEnumerable`2.<>c__DisplayClass3.<GetParseFn>b__0(String value)
at ServiceStack.Text.Common.DeserializeSpecializedCollections`2.<>c__DisplayClass7. <GetGenericEnumerableParseFn>b__6(String x)
at ServiceStack.Text.Json.JsonReader`1.Parse(String value)
at ServiceStack.Text.JsonSerializer.DeserializeFromString[T](String value)
at GitHubCommitAttemptTranslator.Tests.GitHubCommitAttemptTranslatorTests.TranslateFromGitHubToCommitMessage()
And, the second:
dynamic root = ServiceStack.Text.JsonObject.Parse(json);
var summaries = new List<string>();
foreach (var commit in root.commits) // <-- Happens here
'ServiceStack.Text.JsonObject' does not contain a definition for 'commits'
Note: the message is 'string' does not contain a definition for 'commits' if I use code from line one, but change the type to or to instead of
at CallSite.Target(Closure , CallSite , Object )
at System.Dynamic.UpdateDelegates.UpdateAndExecute1[T0,TRet](CallSite site, T0 arg0)
at GitHubCommitAttemptTranslator.Tests.GitHubCommitAttemptTranslatorTests.TranslateFromGitHubToCommitMessage()
After using DynamicJson from .NET 4.0 ServiceStack
Referring to mythz's comment:
This test case works, but if I modify it like below:
var dog = new { Name = "Spot", Parts = new { Part1 = "black", Part2 = "gray" }, Arr = new [] { "one", "two", "three"} };
var json = DynamicJson.Serialize(dog);
var deserialized = DynamicJson.Deserialize(json);
Then, deserialized.Name and Parts are fine, but Arr is of type string.
Also:
If I use ' quotes it doesn't appear to work. Is that normal? json2 works (to the degree that Arr is also still a string), but json3 does not work at all. It just returns
Immediate Window:
deserialized = DynamicJson.Deserialize(json3);
{}
base {System.Dynamic.DynamicObject}: {}
_hash: Count = 1
----- code: -----
var json2 =
#"
{
""Name"": ""Spot"",
""Parts"": {
""Part1"": ""black"",
""Part2"": ""gray""
},
""Arr"": [
""one"",
""two"",
""three""
]
}";
var json3 =
#"
{
'Name': 'Spot',
'Parts': {
'Part1': 'black',
'Part2': 'gray'
},
'Arr': [
'one',
'two',
'three'
]
}";
var deserialized = DynamicJson.Deserialize(json1);
ServiceStack's JSON Serializer also supports dynamic parsing, see examples of how to parse GitHub's JSON in the Dynamic JSON section of the wiki page.

Parsing JSON with Dart

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'].