4

I've been playing with Kotlinx.serialisation. I've been trying to find a quick way to use Kotlinx.serialisation to create a plain simple JSON (mostly to send it away), with minimum code clutter.

For a simple string such as:

{"Album": "Foxtrot", "Year": 1972}

I've been doing is something like:

val str:String = Json.stringify(mapOf(
        "Album" to JsonPrimitive("Foxtrot"),
        "Year" to JsonPrimitive(1972)))

Which is far from being nice. My elements are mostly primitive, so I wish I had something like:

val str:String = Json.stringify(mapOf(
   "Album" to "Sergeant Pepper",
   "Year" to 1967))

Furthermore, I'd be glad to have a solution with a nested JSON. Something like:

Json.stringify(JsonObject("Movies", JsonArray(
   JsonObject("Name" to "Johnny English 3", "Rate" to 8),
   JsonObject("Name" to "Grease", "Rate" to 1))))

That would produce:

{
  "Movies": [
    {
      "Name":"Johnny English 3",
      "Rate":8
    },
    {
      "Name":"Grease",
      "Rate":1
    }
  ]
}

(not necessarily prettified, even better not)

Is there anything like that?

Note: It's important to use a serialiser, and not a direct string such as

"""{"Name":$name, "Val": $year}"""

because it's unsafe to concat strings. Any illegal char might disintegrate the JSON! I don't want to deal with escaping illegal chars :-(

Thanks

Maneki Neko
  • 809
  • 1
  • 9
  • 20
  • What is a "rogue char" and how would you prevent them when using your desired approach? – Tim Mar 29 '19 at 12:10
  • I meant, a char like " or } or chars illegal in JSON as is. I just don't wanna deal with escaping and all that tedious fuss of illegal chars. Anyway, thanks, I will make it clearer. – Maneki Neko Mar 29 '19 at 13:53
  • Is Kotlinx.serialization required? I've found data classes + gson, or jackson to work really well. I'm not sure if this approach would be able to target something other than the JVM though. I'll also just use mapOf and listOf like you show above in spring controllers, and it works great. – aglassman Mar 29 '19 at 14:20
  • Yeah, I'd suggest checking out gson if you just want to serialize random maps, lists, and data classes. One thing to watch out for, which something like Kotlinx.serialization may handle, is default values on data classes. Gson and Jackson don't account for that last time I checked. – aglassman Mar 29 '19 at 14:32
  • No real reason to use Kotlinx.serialisation. Just searched for a lib that walks naturally with Kotlin data classes and gradle plugins. Kotlinx.serialisation is cute, I must say. It just works with Kotlin and its types. Android Studio/Idea autocompletes things nicely... Loved it, generally. – Maneki Neko Mar 29 '19 at 14:35

1 Answers1

6

Does this set of extension methods give you what you want?

@ImplicitReflectionSerializer
fun Map<*, *>.toJson() = Json.stringify(toJsonObject())

@ImplicitReflectionSerializer
fun Map<*, *>.toJsonObject(): JsonObject = JsonObject(map {
    it.key.toString() to it.value.toJsonElement()
}.toMap())

@ImplicitReflectionSerializer
fun Any?.toJsonElement(): JsonElement = when (this) {
    null -> JsonNull
    is Number -> JsonPrimitive(this)
    is String -> JsonPrimitive(this)
    is Boolean -> JsonPrimitive(this)
    is Map<*, *> -> this.toJsonObject()
    is Iterable<*> -> JsonArray(this.map { it.toJsonElement() })
    is Array<*> -> JsonArray(this.map { it.toJsonElement() })
    else -> JsonPrimitive(this.toString()) // Or throw some "unsupported" exception?
}

This allows you to pass in a Map with various types of keys/values in it, and get back a JSON representation of it. In the map, each value can be a primitive (string, number or boolean), null, another map (representing a child node in the JSON), or an array or collection of any of the above.

You can call it as follows:

val json = mapOf(
    "Album" to "Sergeant Pepper",
    "Year" to 1967,
    "TestNullValue" to null,
    "Musicians" to mapOf(
        "John" to arrayOf("Guitar", "Vocals"),
        "Paul" to arrayOf("Bass", "Guitar", "Vocals"),
        "George" to arrayOf("Guitar", "Sitar", "Vocals"),
        "Ringo" to arrayOf("Drums")
    )
).toJson()

This returns the following JSON, not prettified, as you wanted:

{"Album":"Sergeant Pepper","Year":1967,"TestNullValue":null,"Musicians":{"John":["Guitar","Vocals"],"Paul":["Bass","Guitar","Vocals"],"George":["Guitar","Sitar","Vocals"],"Ringo":["Drums"]}}

You probably also want to add handling for some other types, e.g. dates.

But can I just check that you want to manually build up JSON in code this way rather than creating data classes for all your JSON structures and serializing them that way? I think that is generally the more standard way of handling this kind of stuff. Though maybe your use case does not allow that.

It's also worth noting that the code has to use the ImplicitReflectionSerializer annotation, as it's using reflection to figure out which serializer to use for each bit. This is still experimental functionality which might change in future.

Yoni Gibbs
  • 4,441
  • 1
  • 13
  • 31
  • Well, it's prettier than what I did when I gave up. I did it more or less the same without extensions. I guess, this is as good as it gets. – Maneki Neko Mar 30 '19 at 18:44