1

I have various types of events in my application that are represented by Data classes.

All events must have an attribute called EventContent that stores additional data about the event in String format. These events are saved to a database and EventContent is serialized into a JSON via kotlin.x.serialization.

To enforce this, I have used an interface like this on a sample event called ShutdownEvent:

interface Event {
    var eventContent: String
}


data class ShutdownEvent(
    override var eventContent: String
) : Event

Now imagine for the specific event called ShutdownEvent, I know its EventContent must have only 2 attributes: trigger and location, both of which are Strings. How can I enforce that the EventContent follows this structure.

I was hoping for something like this:

data class ShutdownEvent(
    @Serializable(with = ShutdownContentConverter::class) override var eventContent: ShutdownData,
) : Event


@Serializable
data class ShutdownData(
   var trigger: String,
   var location: String,
) 

But this causes problems with my Event interface implementation, because it expects the eventContent attribute to be a String. Is there an easy way to make this work?


Edit:

I would appreciate an answer that allows me to process my events as follows:

fun process(event: Event) {
    if (event is ShutdownEvent) {
        // do something
    }
}
john
  • 1,561
  • 3
  • 20
  • 44

1 Answers1

2

One possible way to deal with this is to make your interface generic in the "event content" type, instead of using String:

interface Event<C> {
    var eventContent: C
}

data class ShutdownEvent(
    @Serializable(with = ShutdownContentConverter::class)
    override var eventContent: ShutdownData,
) : Event<ShutdownData>

@Serializable
data class ShutdownData(
   var trigger: String,
   var location: String,
)

If all possible types of event content have some common properties, you can define a parent EventContent interface like this:

/** A parent interface for all types of event content. */
interface EventContent {
    val someCommonProp: String
}

interface Event<C : EventContent> {
    var eventContent: C
}

data class ShutdownEvent(
    @Serializable(with = ShutdownContentConverter::class)
    override var eventContent: ShutdownData,
) : Event<ShutdownData>

@Serializable
data class ShutdownData(
    override val someCommonProp: String,
    var trigger: String,
    var location: String,
) : EventContent

Side note: I would strongly advise to use val instead of var everywhere here. You probably don't want event data to be mutable. Once the event is created no-one should really modify it.

Joffrey
  • 32,348
  • 6
  • 68
  • 100
  • Thanks a lot for your answer! Perhaps a follow up question: how can I check what type of Event I got if a func expects an `Event` type? With a generic, I cannot simply do `if (event is ShutdownEvent)` – john Jul 25 '22 at 13:56
  • I added an edit at the bottom of the question. Thank you! – john Jul 25 '22 at 14:02
  • *With a generic, I cannot simply do if (event is ShutdownEvent)* - @john why not? This should work fine, because `ShutdownEvent` is not generic itself – Joffrey Jul 25 '22 at 15:08
  • for example imagine I have a fun that saves the event to the DB. The fun signature looks like save(event: Event<*>) and I am trying to save the eventContent attribute with Exposed like this it[eventContent] = Json.encodeToString(event.eventContent). But kotlinx.serialization throws `Serializer for class 'Any' is not found. Mark the class as @Serializable or provide the serializer explicitly.` – john Jul 25 '22 at 15:36
  • I guess the solution is to manually check and cast each object before saving. For example this works fine: `Json.encodeToString((event as ShutdownEvent).eventContent)` Do let me know if there is a way to infer the type automatically :) – john Jul 25 '22 at 15:42
  • 1
    If you actually use the `if (event is ShutdownEvent)` that you mentioned, this type will automatically be inferred within the `if` block without the need for a cast (`as`). – Joffrey Jul 25 '22 at 15:52