I'm parsing strings into JSON objects, and I need to be able to recursively iterate through the attributes of the objects. So I'm trying to create a function which iterates through the attributes of the object, and if an attribute is not a primitive then call the function again (recursion) with the attribute itself.
In Javascript I'd solve it like this:
function forEachAttribute(object) {
for (let key in object) {
let attribute = object[key];
if (typeof attribute === "object") {
forEachAttribute(attribute);
} else {
console.log(key + ": " + attribute);
}
}
}
let myObject = {
innerObject: {
x: 123
},
y: 456
};
forEachAttribute(myObject);
But I'm moving away from Javascript, and trying to learn how to use Kotlin instead. So I found a way to iterate through the attributes of a JSON object.
But I don't quite understand how to determine if the attribute is a primitive or not.
import kotlin.js.Json
fun iterateThroughAttributes(jsonObject: Json) {
for (key in js("Object").keys(jsonObject)) {
val attribute = jsonObject[key]
// How do I determine if the attribute is a primitive, or not?
}
}
fun main (args: Array<String>) {
val someString = "(some json string)"
val jsonObject = JSON.parse<Json>(someString)
iterateThroughAttributes(jsonObject)
}
Can someone help?
You may check if an object is of some type using is which is equivalent of instanceOf in java.
For details, refer this question.
Related
I wrote a function that takes as input a structure (of type Struct) that can contain primitive types as well as Map and Set objects, and converts it into something that can be JSON-serialized. Examples of input:
let a = 'hello'; // -> "hello"
let b = new Map<string, Set<number>>([['a', new Set<number>([1, 3])]]); // -> {"a": [1, 3]}
let c = {a: new Set<number[]>([[1, 2]])}; // -> {"a": [[1, 2]]}
let d = [{e: false}]; // -> [{"e": false}]
However, I find my code particularly verbose, and I am really not sure about its safety:
type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
type Struct = Json | Struct[] | { [key: string]: Struct } | Map<string, Struct> | Set<Struct>;
function isJson(test: any): test is Json {
if (test == null || ['string', 'number', 'boolean'].indexOf(typeof test) != -1)
return true;
if (Array.isArray(test)) {
// if at least one of the values is not JSON serializable, the array is not JSON-serializable
for (let value of test)
if (!isJson(value))
return false;
return true;
}
if (typeof test == 'object') {
// if it is not a plain object, the object is not JSON-serializable
if (Object.getPrototypeOf(test) != null && test.constructor != Object)
return false;
// if there are symbol properties, the object is not JSON-serializable
if (Object.getOwnPropertySymbols(test).length > 0)
return false;
// if at least one of the values is not JSON serializable, the object is not JSON-serializable
for (let [key, value] of Object.entries(test))
if (!isJson(test[key]))
return false;
return true;
}
return false;
}
function toJson(struct: Struct) {
let json: Json = null;
if (isJson(struct))
json = struct;
else if (Array.isArray(struct) || struct instanceof Set) {
json = [];
let structCast = struct instanceof Set ? struct as Set<Struct> : struct as Struct[];
for (let value of structCast)
json.push(toJson(value));
}
else if (Object.getPrototypeOf(struct) == null || struct.constructor == Object || struct instanceof Map) {
json = {};
let structCast = struct instanceof Map ? struct as Map<string, Struct> : Object.entries(struct);
for (let [key, value] of structCast)
json[key] = toJson(value);
}
return json;
}
I am especially annoyed by the isJson function. Is there no way to get rid of it? I know that most of the typechecking is lost after compilation in TypeScript, but is there a better way do what I want?
Thank you for your help.
I'm going to assume that you're writing TypeScript code and the users of toJson() are also writing TypeScript code. If that's not true, and if the toJson() function is called by some pure JavaScript code somewhere, then you lose any compile time guarantees and will need to add as many runtime checks to toJson() as you feel necessary. From here on out I assume all the relevant code will pass through a compile step.
In this case, you can probably stop worrying about some of the strange edge cases you're dealing with inside isJson(). If the TypeScript Struct type definition already prohibits something then you don't need to write runtime code to deal with it. On the other hand, anything that Struct allows but you'd like to disallow will need runtime checks. For example, it's not generally possible in TypeScript to say "reject object types with any symbol-valued keys". You can use generics to make the compiler reject any object types known to have such keys, but this is more complex, and does not work on the general case where an object of type like {a: string} may or may not have additional unknown keys. I would suggest that, if you really need such checks, to do so by throw statement, since you can't catch them before runtime anyway (unless you want toJson() to return Json | undefined or some other failure marker).
So, let's approach this by merging the isJson() checks we need into the toJson() function and eliminating isJson(). Here's one possible implementation of toJson():
function toJson(struct: Struct): Json {
if (struct === null || typeof struct === "string" ||
typeof struct === "number" || typeof struct === "boolean") {
return struct;
}
if (Array.isArray(struct)) {
return struct.map(toJson);
}
if (struct instanceof Set) {
return Array.from(struct).map(toJson);
}
if (struct instanceof Map) {
return Object.fromEntries(
Array.from(struct).map(([k, v]) => [k, toJson(v)])
);
}
return Object.fromEntries(
Object.entries(struct).map(([k, v]) => [k, toJson(v)])
);
}
The four primitive checks at the beginning could be refactored to an array.includes() or similar code, but then the TypeScript compiler won't understand what's going on and you'd need return struct as Json. The way I've got it is more verbose but the compiler is 100% sure that struct is a valid Json inside that block. Either way is fine.
The rest of the checks are just using built in JavaScript functions and methods like Array.from(), Array.prototype.map(), Object.entries() and Object.fromEntries().
As for the typings, if you want, you can give toJson() a call signature that tries to actually map the input type to the output type. For example:
type ToJson<T> =
Struct extends T ? Json :
Json extends T ? T :
T extends Map<infer K, infer S> ? { [P in Extract<K, string>]: ToJson<S> } :
T extends Set<infer S> ? ToJson<S>[] :
T extends object ? { [K in keyof T]: ToJson<T[K]> } :
T;
function toJson<T extends Struct>(struct: T): ToJson<T>;
ToJson<T> is similar to the implementation, but represented as a type operation. If the input type T is just Struct, then output Json. If it's already a subtype of Json, then output T. If it's a Map<K, S>, then produce an object whose keys are K and whose values are ToJson<S>. If it's a Set<S>, then output an array whose elements are ToJson<S>. And if it's an object (arrays are included), then map all the entries via ToJson. Finally, if it's just a primitive type, return the primitive type.
Let's see if this all works:
const foo = {
str: "hello",
num: 123,
boo: true,
nul: null,
jArr: ["one", 2, { x: 3 }],
jObj: { a: "one", b: 2, c: { x: 3 } },
set: new Set([1, 2, 3]),
map: new Map([["a", 1], ["b", 2]])
}
const json = toJson(foo);
The type seen by the compiler is as follows:
/* const json: {
str: string;
num: number;
boo: boolean;
nul: null;
jArr: (string | number | {
x: number;
})[];
jObj: {
a: string;
b: number;
c: {
x: number;
};
};
set: number[];
map: {
[x: string]: number;
};
} */
Note how the set and map properties have been transformed. Let's make sure the implementation worked also:
console.log(json);
/* {
"str": "hello",
"num": 123,
"boo": true,
"nul": null,
"jArr": [
"one",
2,
{
"x": 3
}
],
"jObj": {
"a": "one",
"b": 2,
"c": {
"x": 3
}
},
"set": [
1,
2,
3
],
"map": {
"a": 1,
"b": 2
}
} */
Also good. The fact that the compiler type and the implementation value agree means that you can, if you want, perform other operations on it and get some type checking and hints:
console.log(json.set[0].toFixed(2)); // 1.00
Playground link to code
The problem I am trying to solve is perfectly described by the following text got from this link:
For a concrete example of when this could be useful, consider an API that supports partial updates of objects. Using this API, a JSON object would be used to communicate a patch for some long-lived object. Any included property specifies that the corresponding value of the object should be updated, while the values for any omitted properties should remain unchanged. If any of the object’s properties are nullable, then a value of null being sent for a property is fundamentally different than a property that is missing, so these cases must be distinguished.
That post presents a solution but using the kotlinx.serialization library, however, I must use gson library for now.
So I am trying to implement my own solution as I didn't find anything that could suit my use case (please let me know if there is).
data class MyObject(
val fieldOne: OptionalProperty<String> = OptionalProperty.NotPresent,
val fieldTwo: OptionalProperty<String?> = OptionalProperty.NotPresent,
val fieldThree: OptionalProperty<Int> = OptionalProperty.NotPresent
)
fun main() {
val gson = GsonBuilder()
.registerTypeHierarchyAdapter(OptionalProperty::class.java, OptionalPropertyDeserializer())
.create()
val json1 = """{
"fieldOne": "some string",
"fieldTwo": "another string",
"fieldThree": 18
}
"""
println("json1 result object: ${gson.fromJson(json1, MyObject::class.java)}")
val json2 = """{
"fieldOne": "some string",
"fieldThree": 18
}
"""
println("json2 result object: ${gson.fromJson(json2, MyObject::class.java)}")
val json3 = """{
"fieldOne": "some string",
"fieldTwo": null,
"fieldThree": 18
}
"""
println("json3 result object: ${gson.fromJson(json3, MyObject::class.java)}")
}
sealed class OptionalProperty<out T> {
object NotPresent : OptionalProperty<Nothing>()
data class Present<T>(val value: T) : OptionalProperty<T>()
}
class OptionalPropertyDeserializer : JsonDeserializer<OptionalProperty<*>> {
private val gson: Gson = Gson()
override fun deserialize(
json: JsonElement?,
typeOfT: Type?,
context: JsonDeserializationContext?
): OptionalProperty<*> {
println("Inside OptionalPropertyDeserializer.deserialize json:$json")
return when {
// Is it a JsonObject? Bingo!
json?.isJsonObject == true ||
json?.isJsonPrimitive == true-> {
// Let's try to extract the type in order
// to deserialize this object
val parameterizedType = typeOfT as ParameterizedType
// Returns an Present with the value deserialized
return OptionalProperty.Present(
context?.deserialize<Any>(
json,
parameterizedType.actualTypeArguments[0]
)!!
)
}
// Wow, is it an array of objects?
json?.isJsonArray == true -> {
// First, let's try to get the array type
val parameterizedType = typeOfT as ParameterizedType
// check if the array contains a generic type too,
// for example, List<Result<T, E>>
if (parameterizedType.actualTypeArguments[0] is WildcardType) {
// In case of yes, let's try to get the type from the
// wildcard type (*)
val internalListType = (parameterizedType.actualTypeArguments[0] as WildcardType).upperBounds[0] as ParameterizedType
// Deserialize the array with the base type Any
// It will give us an array full of linkedTreeMaps (the json)
val arr = context?.deserialize<Any>(json, parameterizedType.actualTypeArguments[0]) as ArrayList<*>
// Iterate the array and
// this time, try to deserialize each member with the discovered
// wildcard type and create new array with these values
val result = arr.map { linkedTreeMap ->
val jsonElement = gson.toJsonTree(linkedTreeMap as LinkedTreeMap<*, *>).asJsonObject
return#map context.deserialize<Any>(jsonElement, internalListType.actualTypeArguments[0])
}
// Return the result inside the Ok state
return OptionalProperty.Present(result)
} else {
// Fortunately it is a simple list, like Array<String>
// Just get the type as with a JsonObject and return an Ok
return OptionalProperty.Present(
context?.deserialize<Any>(
json,
parameterizedType.actualTypeArguments[0]
)!!
)
}
}
// It is not a JsonObject or JsonArray
// Let's returns the default state NotPresent.
else -> OptionalProperty.NotPresent
}
}
}
I got most of the code for the custom deserializer from here.
This is the output when I run the main function:
Inside OptionalPropertyDeserializer.deserialize json:"some string"
Inside OptionalPropertyDeserializer.deserialize json:"another string"
Inside OptionalPropertyDeserializer.deserialize json:18
json1 result object: MyObject(fieldOne=Present(value=some string), fieldTwo=Present(value=another string), fieldThree=Present(value=18))
Inside OptionalPropertyDeserializer.deserialize json:"some string"
Inside OptionalPropertyDeserializer.deserialize json:18
json2 result object: MyObject(fieldOne=Present(value=some string), fieldTwo=my.package.OptionalProperty$NotPresent#573fd745, fieldThree=Present(value=18))
Inside OptionalPropertyDeserializer.deserialize json:"some string"
Inside OptionalPropertyDeserializer.deserialize json:18
json3 result object: MyObject(fieldOne=Present(value=some string), fieldTwo=null, fieldThree=Present(value=18))
I am testing the different options for the fieldTwo and it is almost fully working, with the exception of the 3rd json, where I would expect that fieldTwo should be fieldTwo=Present(value=null) instead of fieldTwo=null.
And I see that in this situation, the custom deserializer is not even called for fieldTwo.
Can anyone spot what I am missing here? Any tip would be very appreciated!
I ended giving up of gson and move to moshi.
I implemented this behavior based on the solution presented in this comment.
I am quite new to Kotlin but and I have successfully used Kotlin serialization on many cases - works out of the box even for nested classes, mutableLists etc. However I struggle with two dimensional arrays.
My class:
import kotlinx.serialization.*
#Serializable
data class Thing(val name:String)
#Serializable
data class Array2D(val width:Int, val height:Int,
var arrayContents:Array<Array<Thing?>> = Array(1){Array(1){null} }){
init{
arrayContents = Array(width){Array(height){null} }
}
}
And when doing this:
val a = Array2D(2, 2)
a.arrayContents[0][0] = Thing("T0")
a.arrayContents[0][1] = Thing("T1")
a.arrayContents[1][0] = Thing("T2")
a.arrayContents[1][1] = Thing("T3")
val json = Json {
allowStructuredMapKeys = true
}
val jsonString = json.encodeToString(Array2D.serializer(), a)
assertEquals(
"""
{"width":2,"height":2,"arrayContents":[[{"name":"T0"},{"name":"T1"}],[{"name":"T2"},{"name":"T3"}]]}
""".trim(),
jsonString
) // encoding is OK
val b = json.decodeFromString(deserializer = Array2D.serializer(), jsonString)
// this fails to reproduce "a" and stops at first array level
// b.arrayContents = {Thing[2][]} (array of nulls) instead of {Thing[2][2]} (array of array of Thing)
If it can encode the class to String it should decode it as well, right? Or am I missing something here? Maybe I should use custom serializer but there are not many examples that fit my case. One example is https://github.com/Kotlin/kotlinx.serialization/issues/357 but it is only one level of array.
Thanks for any help :)
Serialization/deserialization for arrays (including multi-dimensional) works out of the box.
Unexpected behavior is related to init section of your data class. It's executed after deserialization happens and overwrites data parsed from JSON.
It also happens when you create instance of Array2D manually:
val x = Array2D(1, 1, Array(1) { Array(1) { Thing("0") } })
println(x.arrayContents[0][0]) //will print null
You just need to replace init block with secondary constructor (default value for arrayContents is redundant, by the way), and you may declare arrayContents as val now:
#Serializable
data class Array2D(val width: Int, val height: Int, val arrayContents: Array<Array<Thing?>>) {
constructor(width: Int, height: Int) : this(width, height, Array(width) { Array(height) { null } })
}
I have a lot of code like this, it is all the same except for the type PositionJson, it could be AnotherJson or FooJson or BarJson
Is there some way I can exctract all this code into one function that I can somehow pass into it the type? So that I don't have several of these big blocks of almost identical code littering my class?
I'm not sure if this is possible or not, just thought I'd ask because it would be nice to do...
/**
* #return the _open_ [PositionJson]s
*/
val positions: Array<PositionJson>?
#Throws(AccountsAPIException::class)
get() {
val service = constructServiceURL(POSITIONS, null, true)
try {
val messageJson = mapper.readValue<MessageJson<Array<PositionJson>>>(
callURL(service),
object: TypeReference<MessageJson<Array<PositionJson>>>() {
})
val error = messageJson.error
if (error != null) throw AccountsAPIException(error.errorCode, error.description)
return messageJson.data
} catch (e: Exception) {
throw AccountsAPIException(e)
}
}
You can do what you want with generics. However, to use generics we first need to extract that giant block of code into a method:
val positions: Array<PositionJson>? get() = getPositions()
fun getPositions(): Array<PositionJson>? {
...
}
We haven't solved the problem, but now we're in a position to be able to solve it by making getPositions generic (note that I also rename the function):
val positions: Array<PositionJson> get() = getArrayOf<PositionJson>()
// thanks to type inference I can omit the type on getArrayOf if desired:
val positions: Array<PositionJson> get() = getArrayOf()
fun <T> getArrayOf(): Array<T>? {
val service = constructServiceURL(POSITIONS, null, true)
try {
val messageJson = mapper.readValue<MessageJson<Array<T>>>(
callURL(service),
object: TypeReference<MessageJson<Array<T>>>() {
})
val error = messageJson.error
if (error != null) throw AccountsAPIException(error.errorCode, error.description)
return messageJson.data
} catch (e: Exception) {
throw AccountsAPIException(e)
}
}
Perfect! Except this won't compile thanks to type erasure. But we can fix this too by making the function inline and making the type parameter reified:
inline fun <reified T: Any> getArrayOf(): Array<T>? {
...
}
And that should do it. Now you can reuse this function as needed:
val positions: Array<PositionJson>? get() = getArrayOf()
val persons: Array<PersonJson>? get() = getArrayOf()
val bananas: Array<BananaJson>? get() = getArrayOf()
inline fun <reified T: Any> getArrayOf(): Array<T>? {
val service = constructServiceURL(POSITIONS, null, true)
try {
val messageJson = mapper.readValue<MessageJson<Array<T>>>(
callURL(service),
object: TypeReference<MessageJson<Array<T>>>() {
})
val error = messageJson.error
if (error != null) throw AccountsAPIException(error.errorCode, error.description)
return messageJson.data
} catch (e: Exception) {
throw AccountsAPIException(e)
}
}
One last thing: note that in all my examples I used property getters (get() = ...) as in your original code. However, I strongly suspect that you do NOT want to use a getter. Getters will be called every time someone accesses your property, which in this case means that every time someone reads the positions property you'll be calling constructServiceURL and making the service call, etc. If you want that code to only happen once then you should just call getArrayOf() once and assign the result to your property:
val positions: Array<PositionJson>? = getArrayOf()
// this syntax would also work:
val positions = getArrayOf<PositionJson>()
I ran through the standard JSON library of Rust http://doc.rust-lang.org/serialize/json/ and couldn't figure out what represents a node in it. In Java it's JsonNode. What's it in Rust? For example, how can I pass an argument of the type JsonNode in Rust?
Rust's "DOM" for JSON is defined by Json enum. For example, this JSON object:
{ "array": [1, 2, 3], "submap": { "bool": true, "string": "abcde" } }
is represented by this expression in Rust:
macro_rules! tree_map {
($($k:expr -> $v:expr),*) => ({
let mut r = ::std::collections::TreeMap::new();
$(r.insert($k, $v);)*
r
})
}
let data = json::Object(tree_map! {
"array".to_string() -> json::List(vec![json::U64(1), json::U64(2), json::U64(3)]),
"submap".to_string() -> json::Object(tree_map! {
"bool".to_string() -> json::Boolean(true),
"string".to_string() -> json::String("abcde".to_string())
})
});
(try it here)
I've used custom map construction macro because unfortunately Rust standard library does not provide one (yet, I hope).
Json is just a regular enum, so you have to use pattern matching to extract values from it. Object contains an instance of TreeMap, so then you have to use its methods to inspect object structure:
if let json::Object(ref m) = data {
if let Some(value) = m.find_with(|k| "submap".cmp(k)) {
println!("Found value at 'submap' key: {}", value);
} else {
println!("'submap' key does not exist");
}
} else {
println!("data is not an object")
}
Update
Apparently, Json provides a lot of convenience methods, including find(), which will return Option<&Json> if the target is an Object which has corresponding key:
if let Some(value) = data.find("submap") {
println!("Found value at 'submap' key: {}", value);
} else {
println!("'submap' key does not exist or data is not an Object");
}
Thanks #ChrisMorgan for the finding.