TypeScript function to convert JSON - json

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

Related

How to use Gson to deserialize a json array string to object mode using generic class type?

Having a function for deserializing json string into object mode by providing the class type the json string should be deserialized into.
It works for most of the case
fun <T> deserializeJsonStr(jsonString: String, dataClassType: Class<T>):T? {
var ret: T? = Gson().fromJson<T>(jsonString, dataClassType)
return ret
}
like
// data modle
class TestData1 {
var userId = 0
var id = 0
var title: String? = null
var body: String? = null
}
// the json string
{
"userId": 1,
"id": 3,
"title": "the title",
"body": "the body"
}
// call the function
val clazz: Class<TestData1> = TestData1::class.java
val theTestData1 = deserializeJsonStr(jsonString, clazz)
it will return the object theTestData1 with the fields filled.
But if the json string is for json array:
[
{
"userId": 1,
"id": 1,
"title": "title1",
"body": "body1"
},
{
"userId": 1,
"id": 2,
"title": "title2",
"body": "body2"
},
{
"userId": 1,
"id": 3,
"title": "title3",
"body": "body3"
}
]
Though it should be ArrayList<TestData1>, but what is it class type? tried:
val clazz: Class<ArrayList<TestData1>> = ArrayList<TestData1>::class.java
val theTestData1 = psreJsonStr(jsonString, clazz)
but it does not compile:
or the val pojoClass: Class<ArrayList<TestData1>> = ArrayList<>::class.java does not compile either:
what would be the data class mode for this json array? or what is the Class<T> required by the function param for ArrayList?
This is related to type erasure: ArrayList<TestData1>::class.java is not possible because at runtime the type argument of ArrayList is not available, so this would basically be Class<ArrayList>1. Therefore the compiler does not allow this because it would not be safe.
Gson has its TypeToken class to work around this, see also the user guide.
In your case you could change your deserializeJsonStr to the following:
fun <T> deserializeJsonStr(jsonString: String, dataType: TypeToken<T>): T? {
// Note: Older Gson versions might require using `dataType.type`
return Gson().fromJson(jsonString, dataType)
}
You would then call the function like this:
val type = object: TypeToken<ArrayList<TestData1>>() {}
val data = deserializeJsonStr(jsonString, type)
You could additionally add an overload of deserializeJsonStr with reified type parameter which makes it make more convenient to use:
inline fun <reified T> deserializeJsonStr(jsonString: String): T? {
return deserializeJsonStr(jsonString, object: TypeToken<T>() {})
}
// Note: You can even omit the explicit `ArrayList<TestData1>` if the compiler can infer it
val data = deserializeJsonStr<ArrayList<TestData1>>(jsonString, type)
1 Class is a bit special because its direct type argument is not erased at runtime, for example Class<ArrayList> is not erased to Class<Object>. But for every other generic type the type argument is erased.

Trying to loop over JSON keys

i have been wondering how to solve the following problem.
you have a JSON like this and you parse using serde.
{
"student_name": "this guy",
"subjects": [{"Chemistry": 0},
{"Politics": 0},
{"Biology": 0},
{"Art": 0}],
}
then use a struct to map the types
struct Environment {
student_name: String,
subjects: Vec<HashMap<String, i32>>,
}
but when you try to loop over the keys from the
subjects Vector you get all kinds of nasty errors like this one
error[E0308]: mismatched types
--> src/main.rs:15:9
|
15 | for (key, val) in json.subjects.iter() {
| ^^^^^^^^^^ -------------------- this expression has type std::option::Option<&HashMap<std::string::String, i32>>`
| |
| expected struct `HashMap`, found tuple
|
note: expected struct `HashMap<std::string::String, i32>`
found tuple `(_, _)`
this is all the code, i would really be greatful of someone helping me out. i really lost all hope; thank you beforehand.
use serde_derive::Deserialize;
use serde_json::Result;
use std::{collections::HashMap, fs};
#[derive(Deserialize, Clone)]
struct Environment {
student_name: String,
subjects: Vec<HashMap<String, i32>>
}
fn data_parser() -> Result<()> {
let data = fs::read_to_string("./src/data.json").unwrap();
let json = serde_json::from_str::<Environment>(&data)?;
for (key, val) in json.subjects.iter() {
println!("{}", key);
println!("----------------------");
}
Ok(())
}
fn main() {
data_parser();
}
You collection is of type Vec<HashMap<String, i32>>, so you are given references to HashMap<String, i32> when iterating, you can flat_map over those as iterators also to actually get what you need:
for (key, val) in json.subjects.iter().flat_map(|d| d.iter()) {
println!("{}", key);
println!("----------------------");
}
Playground
A subject is a <HashMap<String, i32>. Try iterating through every subject.
fn data_parser() -> Result<()> {
let data = fs::read_to_string("./src/data.json").unwrap();
let json = serde_json::from_str::<Environment>(&data)?;
for subject in json.subjects.iter() {
for (_key, value) in subject {
println!("{}", value);
println!("----------------------");
}
}
Ok(())
}
Or, better yet, you can "forget" about loops altogether:
json.subjects
.iter()
.flat_map(|s| s.iter())
.for_each(|(key, value)| {
println!("{key}: {value}");
println!("----------------------");
});

Gson deserialization of optional+nullable value

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.

How to iterate through attributes of a JsonObject recursively?

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.

JsonNode in JSON in Rust

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.