← back
RSS

Mapping HTTP request to sealed class with Jackson

3 min read ·

TL;DR

Today I learned how to map http request to sealed class in Kotlin and decided to share that with you.

Context

Sometimes it can happen that our REST API consumes jsons that have conditional fields. Let’s say we have a following endpoint in our application:

POST /bet-offers

It is used for adding a new bet offer to our application which offers betting on different sport events. There are different types of bet offers:

There can be many other bet types, but those three are going serve as our example.

For the first case request can look like that (very simplified version):

{
  "betType": "WIN_LOSE",
  "winLoseBetOffer": {
    "homeTeamWins": {
      "amount": "2.22",
      "currency": "PLN"
    },
    "awayTeamWins": {
      "amount": "2.46",
      "currency": "PLN"
    }
  }
}

For the second case request can look like that (very simplified version):

{
  "betType": "WIN_DRAW_LOSE",
  "winDrawLoseBetOffer": {
    "homeTeamWins": {
      "amount": "2.22",
      "currency": "PLN"
    },
    "draw": {
      "amount": "1.86",
      "currency": "PLN"
    },
    "awayTeamWins": {
      "amount": "2.46",
      "currency": "PLN"
    }
  }
}

For the third case request can look like that (very simplified version):

{
  "betType": "AMOUNT_OF_GOALS",
  "amountOfGoals": {
    "moreThan": [
      {
        "amount": "1",
        "odds": {
          "amount": "1.59",
          "currency": "PLN"
        }
      },
      ...
    ],
    "lessThan": [
      {
        "amount": "5",
        "odds": {
          "amount": "1.24",
          "currency": "PLN"
        }
      },
      ...
    ]
  }
}

So all of those requests have the same field: betType, however next field depends of the value of the first field - you got the idea.

Naive solution

Until today I would do something like that:

data class BetOfferDto(
    val betType: BetType,
    val winLoseBetOffer: WinLoseBetOffer?,
    val winDrawLoseBetOffer: WinDrawLoseBetOffer?,
    val amountOfGoals: AmountOfGoalsBetOffer?
)
    @PostMapping
    fun addBetOffer(betOffer: BetOfferDto) {
        when(betOffer.betType) {
            BetType.WIN_LOSE -> WinLoseBetOffer.from(betOffer)
            BetType.WIN_DRAW_LOSE -> WinDrawLoseBetOffer.from(betOffer)
            BetType.AMOUNT_OF_GOALS -> AmountOfGoalsBetOffer.from(betOffer)
        }
    }

and in from method I would do requireNotNull on specific field and do all the mapping. Not the best solution but it does the job.

Elegant solution

Today I found out that there is much easier way for that.

sealed class BetOfferDto
data class WinLoseBetOfferWrapper(
    val winLoseBetOffer: WinLoseBetOffer
) : BetOfferDto()

data class WinLoseBetOffer(
    val homeTeamWins: Odds,
    val awayTeamWins: Odds
)
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "betType")
@JsonSubTypes(
    JsonSubTypes.Type(value = WinLoseBetOfferWrapper::class, name = "WIN_LOSE"),
    JsonSubTypes.Type(value = WinDrawLoseBetOfferWrapper::class, name = "WIN_DRAW_LOSE"),
    JsonSubTypes.Type(value = AmountOfGoalsBetOfferWrapper::class, name = "AMOUNT_OF_GOALS")
)
sealed class BetOfferDto

And we don’t need to worry about all the mapping! Our controller looks much better:

    @PostMapping("/bet-offers")
    fun addBetOffer(@RequestBody betOffer: BetOfferDto) {
        when (betOffer) {
            is WinLoseBetOfferWrapper -> TODO("WinLoseBetOfferWrapper")
            is WinDrawLoseBetOfferWrapper -> TODO("WinDrawLoseBetOfferWrapper")
            is AmountOfGoalsBetOfferWrapper -> TODO("AmountOfGoalsBetOfferWrapper")
            null -> TODO("null")
        }
    }

Additionally it is worth to mention that @JsonSubtypes works fine on nullable fields.

Note: Remember about adding jackson-module-kotlin dependency, so request can be bound to data class.

Full example can be found here: https://github.com/piotr-proszowski/mapping-requests-to-sealed-class/

That’s all for today, happy coding!

Comments