Using kotlinx.serialization to deserialize a JSON array into a sealed class

Stored as nested JSON arrays, my data is in rich text format. The plaintext of the string and annotations describing the formatting are stored in text tokens. At decode time, I aim to map the specific structure of these nested JSON arrays to a rich Kotlin class hierarchy.

The TypeScript type below describes this text encoding:

// Text string is an array of tokens
type Text = Array<TextToken>
// Each token is a Array[2] tuple. The first element is the plaintext.
// The second element is an array of annotations that format the text.
type TextToken = [string, Array<Annotation>]
// My question is about how to serialize/deserialize the Annotation type
// to a sealed class hierarchy.
//
// Annotations are an array where the first element is always a type discriminator string
// Each annotation type may have more elements, depending on the annotation type.
type Annotation =
 | ["b"] // Text with this annotation is bold
 | ["i"] // Text with this annotation is italic
 | ["@", number] // User mention
 | ["r", { timestamp: string, reminder: string }] // Reminder

To represent the same thing using Kotlin classes with sealed class, I have defined the following structured output format after deserializing the JSON:

// As JSON example: [["hello ", []], ["Jake", [["b"], ["@", 1245]]]]
data class TextValue(val tokens: List<TextToken>)

// As JSON example: ["hello ", []]
// As JSON example: ["Jake", [["b"], ["@", 1245]]]
data class TextToken(val plaintext: String, val annotations: List<Annotation>)

sealed class Annotation {
  // As JSON example: ["b"]
  @SerialName("b")
  object Bold : Annotation()

  // As JSON example: ["i"]
  @SerialName("i")
  object Italic : Annotation()

  // As JSON example: ["@", 452534]
  @SerialName("@")
  data class Mention(val userId: Int)

  // As JSON example: ["r", { "timestamp": "12:45pm", "reminder": "Walk dog" }]
  @SerialName("r")
  data class Reminder(val value: ReminderValue)
}

How should I define my serializers? I attempted defining a serializer using JsonTransformingSerializer, but encountered a null pointer exception when trying to wrap the default serializer for one of my classes:

@Serializable(with = TextValueSerializer::class)
data class TextValue(val tokens: List<TextToken>)

object TextValueSerializer : JsonTransformingSerializer<TextValue>(TextValue.serializer()) {
    override fun transformDeserialize(element: JsonElement): JsonElement {
        return JsonObject(mapOf("tokens" to element))
    }

    override fun transformSerialize(element: JsonElement): JsonElement {
        return (element as JsonObject)["tokens"]!!
    }
}
Caused by: java.lang.NullPointerException: Parameter specified as non-null is null: method kotlinx.serialization.json.JsonTransformingSerializer.<init>, parameter tSerializer
    at kotlinx.serialization.json.JsonTransformingSerializer.<init>(JsonTransformingSerializer.kt)
    at example.TextValueSerializer.<init>(TextValue.kt:17)

Answer №1

The issue you are experiencing appears to be due to referencing the TextValue serializer within the TextValue serializer.

Because the data structure does not align perfectly with the key:value pairings expected by the serializer, it becomes more challenging to automate this process.

For your current setup, here is what you will need to do, starting from the bottom up:

  1. Annotation

    You can create a custom serializer that converts the representation of JsonArray to its corresponding Annotation format. This involves mapping the indices of the JsonArray to their respective sealed class representations. By using the first index as the discriminator, we can determine the type we are mapping to.

    In cases where possible, utilizing the auto-generated serializers is recommended.

    []          -> Annotation.None
    ["b"]       -> Annotation.Bold
    ["@", 1245] -> Annotation.Mention
    ...
    

    To achieve this, create a new serializer and associate it with the Annotation class (

    @Serializable(with = AnnotationSerializer::class)
    ).

    object AnnotationSerializer : KSerializer<Annotation> {
        // Implementation details for serializing and deserializing Annotations
    }
    
  2. TextToken

    The approach for handling TextToken mirrors that of Annotation. Extracting the token at the first index and building the annotations using the second index. Annotate the TextToken class to use the following serializer:

    object TextTokenSerializer : KSerializer<TextToken> {
        // Implementation details for serializing and deserializing TextToken
    }
    

    Consider returning JSON like

    { plaintext: "Jake", annotations: [["b"], ["@", 1245]] }
    , which aligns better with the TextToken POJO and avoids the need for such serialization.

  3. TextValue

    The final component is the TextValue object, which essentially wraps the list of TextTokens. It may be beneficial to use a type alias for this purpose:

    typealias TextValue = List<TextToken>
    

    One option is to utilize a serializer that translates the JsonArray into the List<TextToken> and then encompasses that list in the TextValue object.

    object TextValueSerializer : KSerializer<TextValue> {
        // Implementation details for serializing and deserializing TextValue
    }
    

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Having trouble accessing a JSON object with Typescript in an Angular 2 project

Something strange is happening with my code. I am working with a JSON object: {"login":"admin","name":"Admin"} And this is the relevant part of my code: private _userData: User; ... private getUserData() { this._userInfoService.getUserInfo() ...

Testing the Child Component's EventEmitter Functionality

In my child component, there is an event emitter that sends a boolean value when the style changes in the parent component: export class ExampleComponent implements OnInit, OnChanges { @Output() statusEvent = new EventEmitter<boolean>(); getS ...

Best method for storing data: opt for JSON or SQLite

Can you tell me the standard or preferred method of operation? In order for the iOS app to function, it requires data. This data can be sourced from an embedded sqlite database or through an online json feed. What is typically the recommended approach? Is ...

Can Angular 9 be used to compile a latex document?

Is it possible to utilize Angular 9 to compile and generate PDF files using latex? Specifically, I am curious about how to compile a document using Angular and Pdflatex. The idea is for the client to input their data in the form of a JSON data structure ...

The triggering of routing in Next.js is not established by useEffect

I'm facing an issue with my Next.js dynamic page that uses routing based on steps in the state. The route is supposed to change whenever a step value changes, like from null to "next" or back. However, the useEffect hook doesn't seem to be reacti ...

What is the best way to make the current tab stand out?

I have implemented a TabHeader component to create a dynamic Tab Menu that displays table contents based on months. The loop runs from the current month back to January, and the content is updated dynamically through an API call triggered by changes in the ...

Is it possible to restrict optionality in Typescript interfaces based on a boolean value?

Currently, I am working on an interface where I need to implement the following structure: export interface Passenger { id: number, name: string, checkedIn: boolean, checkedInDate?: Date // <- Is it possible to make this f ...

What is the best method for compressing and decompressing JSON data using PHP?

Just to clarify, I am not attempting to compress in PHP but rather on the client side, and then decompress in PHP. My goal is to compress a JSON array that includes 5 base64 images and some text before sending it to my PHP API. I have experimented with l ...

Creating a versatile TypeScript Record that can accommodate multiple data types

I have a question regarding the use of Record in TypeScript. When I specify Record as the parameter type in a function, I encounter an error in my code because it does not allow different types. type Keys = 'name' | 'qty'; const getVal ...

A guide on updating or adding a row in a MySQL table with a JSON column using Java SprinBoot CrudRepository

I have been attempting to add a row to a MySQL JSON column using Java SpringBoot CrudRepository. Despite searching through various sources, I have not been able to find a solution to this issue. The custom query I defined is as follows: @Modifying @Tra ...

What is the ideal configuration for Typescript within ASP.NET 4 MVC 5 on Visual Studio 2015?

Currently, I am in the process of integrating a TypeScript project into a VS2015 MVC 5 project (which is based on ASP.NET 4, specifically not asp.net 5 or asp.net 6 - only the MVC portion is version 5). All aspects of my query pertain solely to this target ...

Utilizing JsonNode from Jackson library in Java for parsing nested JSON data can sometimes lead to NullPointerExceptions

I've been struggling to make this parser work properly for some time now, encountering the same issue repeatedly. The code consistently returns a value of null. The problem lies in a call to the Google Books API, which returns a large JSON response w ...

Decode JSON information into a designated structure

I have a JSON data that I want to unmarshal in Go: b := []byte(`{"Asks": [[21, 1], [22, 1]] ,"Bids": [[20, 1], [19, 1]]}`) My current approach involves defining a struct like this: type Message struct { Asks [][]float64 `json:"Bids"` Bids [][]fl ...

Finding a JSON file within a subdirectory

I am trying to access a json file from the parent directory in a specific file setup: - files - commands - admin - ban.js <-- where I need the json data - command_info.json (Yes, this is for a discord.js bot) Within my ban.js file, I hav ...

The React Fabric TextField feature switches to a read-only mode once the value property is included

I've been grappling with how to manage value changes in React Fabric TextFields. Each time I set the value property, the component goes into read-only mode. When utilizing the defaultValue property, everything functions correctly, but I require this i ...

What is the process for defining a default value for a template-driven form input in Angular 2?

I have a simple input element in my form that requires a default initial value to be set. <input type="number" name="interest_rate" [(ngModel)]="interest_rate"> In my code, I included this.form.controls['interest_rate'].patchValue(this.a ...

Tips for fixing the error "Module cannot be found" when testing Typescript in a Github Action

Whenever I use the Github Actions Typescript template to create a new repo and then check it out locally, I face a recurring issue: While I can work on the source code in VS Code without any problems and follow the steps mentioned in the template's re ...

Unit tests are failing to typecast the Angular HTTP GET response in an observable

I've been delving into learning about unit testing with Angular. One of the challenges I encountered involved a service method that utilizes http.get, pipes it into a map function, and returns a typed observable stream of BankAccountFull[]. Despite ...

Postman makes sending JSON requests via cURL seamless

I'm currently attempting to make a request using Postman to an API. Below is the request type that the API accepts: curl https://api.shoonya.com/NorenWClientTP/QuickAuth \ -d "jData={ \"apkversion\": \"1.0.0&bso ...

AngularJS is behaving in a way that requests fresh JSON data only during the initial loading

I'm currently utilizing AngularJS to incorporate an app into a native iOS application, but I'm encountering difficulties with loading dynamic data. I have configured the controllers to retrieve JSON data from the iOS app via http queries for eac ...