Kotlin Sealed Interfaces with KotlinX Serialization JSON

I heavily use sealed interfaces to model result objects in Kotlin as they allow me to create a type of classes that can be handled using exhaustive when statements, similar to an enum, but also each type can contain its own properties.

I wanted to serialize these sealed interface Kotlin models to/from JSON over HTTP. There are a bunch of options for serializing JSON in Java like Moshi, Gson and Jackson. While all of those libraries are great, I had a requirement of creating a multi-platform library, and went with KotlinX Serialization.

In this post I’ll walk you through an example of how I configured KotlinX Serialization to work for my use case.

Example: Marketing Campaigns API Result

This endpoint returns a strongly typed campaign, and I wanted to represent this in JSON.

public sealed interface CampaignContent {
    public data class PopupModal(
        public val imageUrl: String,
        public val text: String,
        public val subtext: String,
    ) : CampaignContent

    public data class Link(
        public val linkText: String,
        public val url: String,
        public val linkIcon: String? = null,
    ) : CampaignContent
}
{
  "type": "popup_modal",
  "image_url": "https://...",
  "text": "Text",
  "subtext": "Subtext"
}
{
  "type": "link",
  "link_icon": "https://...",
  "url": "https://..."
}

We need to deserialize a JSON response into a strongly typed object that implements the CampaignContent sealed interface.

fun getCampaignContentFromServer() : CampaignContent

KotlinX Serialization has Polymorphism support allows us to do this. You need to register polymorphic definitions in a SerializersModule that you provide to your Json object that is used to encode and decode objects to/from JSON.

val jsonSerializer = Json {
  serializersModule = SerializersModule {
    polymorphic(
      CampaignContent::class,
      CampaignContent.PopupModal::class,
      CampaignContent.PopupModal.serializer(),
    )
    polymorphic(
      CampaignContent::class,
      CampaignContent.Link::class,
      CampaignContent.Link.serializer(),
    )
  }
}
val campaignContent : CampaignContent = jsonSerializer.decodeFromString(
  CampaignContent.serializer(), 
  jsonString,
)

In order to support polymorphism, a type property is used in the JSON string representation {"type": "..."}. By default this "type" field is a fully qualified classname. This allows KotlinX Serialization know what type to deserialize. You have control over what the name of this classDiscriminator field is, as well as other configuration options when configuring your Json {} serializer.

If you don’t want to use the fully qualified class name as the class type, then you can put a @SerializedName("...") annotation to the class and it will use that name instead of the fully qualified class name. This is helpful for me as the backend did not use fully qualified names, and I had set them explicitly. In the example below I added the @SerializedName("popup_modal") data class.

Final Models after adding @Serializable and @SerializedName

public sealed interface CampaignContent {

  @Serializable
  @SerializedName("popup_model")
  public data class PopupModal(
    @SerializedName("image_url")
    public val imageUrl: String,
    @SerializedName("text")
    public val text: String,
    @SerializedName("subtext")
    public val subtext: String,
  ) : CampaignContent

  @Serializable
  @SerializedName("link")
  public data class Link(
      @SerializedName("link_text")
      public val linkText: String,
      @SerializedName("url")
      public val url: String,
      @SerializedName("link_icon")
      public val linkIcon: String? = null,
  ) : CampaignContent
}

Considerations

At first I made my models match the JSON values as I didn’t have to specify @SerializedName since KotlinX Serialization will just match the field name. After a bit of usage, link.link_text just didn’t feel as correct as link.linkText, so I chose to specify a @SerializedName annotation instead. The resulting Java bytecode is the same as the KotlinX Serialization plugin does code generation that writes out the serializer anyways. This does make your data class look not as pretty, but from the general building and usage perspective of these models, the user will not know.

Conclusion

That was a whirlwind intro, but I had to really dig through deep into the documentation to figure it out and am hoping this helps someone do this faster than I did it originally.

Kotlin Multiplatform: Building a “Fat” iOS Framework for iosArm64 and iosX64

If you are building a Kotlin Multiplatform library which will be consumed by an existing iOS application, using a framework is a great way to do this. Frameworks are typically compiled for a specific target architecture which can then be run on an iPhone, iPad or iOS Simulator on your Intel Macbook.

If you are going use your Kotlin Multiplatform library as a framework in an existing app, you will want to provide a “Fat” framework which will contain both Arm64 and X64 architectures. This article contains the configuration I used to build the “Fat” framework. This is not a full build.gradle.kts file, but just the additional parts needed for a Kotlin Multiplatform project to build a “Fat” framework.

⚠️Use an XCFramework instead ⚠️

This “Fat” framework method no longer works with XCode 12+. Use an XCFramework instead.


I ended up with this Error Message in XCode 12.4: “Building for iOS Simulator, but the linked and embedded framework ‘my_library.framework’ was built for iOS + iOS Simulator.”

This Stack Overflow post shows you how to create an XCFramework from your two frameworks, and I’ll follow up with a blog post on how to do it with an XCFramework a bit later. This solutions ends up combining the two frameworks into a single XCFramework.

xcrun xcodebuild -create-xcframework \
    -framework /path/to/ios.framework \
    -framework /path/to/sim.framework \
    -output combined.xcframework

Custom Gradle Task to Build “Fat” framework

import org.jetbrains.kotlin.gradle.tasks.FatFrameworkTask

kotlin {
    // Set a name for your framework in a single place and reuse the variable
    val libName = "my_library"

    // Configure your Kotlin Multiplatform lib to generate iOS binaries
    // NOTE: This will only work on Macs
    ios {
        binaries.framework(libName)
    }

    // You can choose your output directory
    val frameworkDestinationDir = buildDir.resolve("cocoapods/framework")

    tasks {

        // Custom task to build the DEBUG framework
        // ./gradlew universalFrameworkDebug
        register("universalFrameworkDebug", FatFrameworkTask::class) {
            baseName = libName
            from(
                iosArm64().binaries.getFramework(libName, "Debug"),
                iosX64().binaries.getFramework(libName, "Debug")
            )
            destinationDir = frameworkDestinationDir
            group = libName
            description = "Create the debug framework for iOS"
            dependsOn("linkDebugFrameworkIosArm64")
            dependsOn("linkDebugFrameworkIosX64")
        }

        // Custom task to build the RELEASE framework
        // ./gradlew universalFrameworkRelease
        register("universalFrameworkRelease", FatFrameworkTask::class) {
            baseName = libName
            from(
                iosArm64().binaries.getFramework(libName, "Release"),
                iosX64().binaries.getFramework(libName, "Release")
            )
            destinationDir = frameworkDestinationDir
            group = libName
            description = "Create the release framework for iOS"
            dependsOn("linkReleaseFrameworkIosArm64")
            dependsOn("linkReleaseFrameworkIosX64")
        }
    }
}

Here are two custom gradle tasks that build a “Fat” framework for debug or release. In this I have it outputting to the build/cocoapods/framework directory, but you can configure that as you like.

Gradle Task for “Fat” iOS framework

  • Build a “Fat” debug version of the framework
    • ./gradlew universalFrameworkDebug
  • Build a “Fat” release version of the framework
    • ./gradlew universalFrameworkRelease

Importing the iOS Framework into XCode

I previously wrote a blog post about how to do this which has a companion video along with it. 👇

Thanks and Related Resources

I didn’t figure this all out myself. I just got it to work for me and extracted out the bare minimum you need to make this work. Thanks to Swapnil Patil for letting me know that “Fat” frameworks are possible. Thanks so much to Marco Gomiero for his post Introducing Kotlin Multiplatform in an existing project.

How to Install a Specific Ruby Version for Cocoapods

After a few wrong turns, the way I was able to get the correct version of Ruby to install on my Macbook was using rvm. Use the steps below. If you run into issues, you may need to install homebrew as well.

  1. Install RVM
    • curl -sSL https://get.rvm.io | bash
  2. Install the Ruby version you need (x.x.x).
    • rvm installĀ 2.7.3
    • If you want to set that version as default, run rvm use 2.7.3 --default
    • More docs on rvm: https://rvm.io/rvm/install
  3. Install bundler
    • gem install bundler

That should be it! Now you should be able to run bundle install in your iOS project directory on the terminal, and you’re good to go from there!

Run Custom Gradle Task After “build”

After running ./gradlew build on my Kotlin Multiplatform project, I wanted to copy the JavaScript build artifacts (.js & .html) to publish a demo where someone could test my library via a web browser. A custom Gradle task is is a great way to write your own behavior to accomplish this using the Gradle build system.

My Custom Gradle Task

I found the Gradle documentation for how to copy files, and was able to write this custom task "myTaskName" to do it. You can then just run ./gradlew myTaskName and it’ll run the task independently. The problem was that I didn’t know how to get it to always run after ./gradlew build ran.

/** Copies files from "build/distributions" to "demo" directory */
tasks.register<Copy>("myTaskName") {
    println("Copying Build Artifacts!!!")
    from(layout.buildDirectory.dir("distributions"))
    include("**/*.*")
    into(layout.buildDirectory.dir("../demo"))
}

TL;DR – Use finalizedBy()

  • finalizedBy – Runs my task AFTER “build”. ✅
    • tasks.named("build") { finalizedBy("myTaskName") }
  • dependsOn – Runs “build” BEFORE my task, if my task is executed explicitly.
    • tasks.named("myTaskName") { dependsOn("build") }

To give you more details and to go into my process of how I figured it out, here are a few options I tried while figuring out how to run this custom task after every execution of the “build” Gradle task.

1. finalizedBy ✅

You can use finalizedBy() to say what task you should run after a named task. I think this reads nicely because it calls out the task dependency separately from it’s declaration. This will appropriately run after the build task executes as I needed.

tasks.named("build") { finalizedBy("myTaskName") }

2. shouldRunAfter 🤔

Another possible way to do this is with shouldRunAfter() which can just be added inside the block where you register your custom task. This will run after the build task executes. HOWEVER: I had to call .get() after registering my task to get it to work in order to have this actually run, which just feels wrong… Someone feel free to explain why, but I’m guessing this is some sort of lazy initialization happening if I don’t call “.get()”. Because of this, I don’t like this solution personally.

tasks.register<Copy>("myTaskName") {
    shouldRunAfter("build") // Tells Gradle to execute after "build" task
    // My Custom Task Code
}.get()

3. dependsOn() 🙃

You would think dependsOn() may work, but it’s the opposite behavior than what I wanted. It’s saying that “myTaskName” needs “build” to have run before it executes. It’s the opposite task dependency relationship from finalizedBy(). This wasn’t the behavior I wanted for this use case because I wanted it to copy the artifacts after every time a ./gradlew build was run, but could be useful depending on your use case.

tasks.named("myTaskName") { dependsOn("build") }

Wrap Up

None of this is rocket science, but if you try to Google for how to do it, it may take you a while to figure out how to do it. Using Option 1, finalizedBy() was the solution that worked for my use case! I hope this saved you an hour or more!

Intro to Kotlin Multiplatform JavaScript

One of the compilation targets of Kotlin Multiplatform is JavaScript. It’s pretty awesome that I can take the same Kotlin code and tools I write Android with, and write code that runs in a browser (or in Node.js).

Here is the video I’ve created to walk you through the creation of a Kotlin Multiplatform Project using the templates in Intellij IDEA, and what you can do to get your Kotlin running as JavaScript in the Browser!

Here are the code snippets mentioned in the video:

./gradlew build
 js(LEGACY) {
     browser {
         webpackTask {
             output.libraryTarget = "this" // Will add to window
         }
         binaries.executable()
     }
 }
 fun printHi() {
     println("Hello World Sam!")
 }
 fun main(args: Array) {
     printHi()
 }
 helloworldsam.printHi()