How to add a custom marshaller to akka http?
Asked Answered
L

3

14

As a beginner to both scala and akka-http, I am trying to hook into the serialization aka marshalling process.

The project uses [email protected] and [email protected]". Furthermore, it has the akka-http-spray-json dependency included.

In the codebase, we use Java.Util.Currency (It may be deprecated which is not important as I'd still like to know how to add a custom marshaller.)

Given this example controller:

def getCurrencyExample: Route = {
    path("currencyExample") {
      val currency: Currency = Currency.getInstance("EUR")
      val code: String = currency.getCurrencyCode

      val shouldBeFormated = collection.immutable.HashMap(
        "currencyCode" -> code,
        "currencyObject" -> currency
      )

      complete(shouldBeFormated)
    }
  }

I get a response like this where the currency object becomes empty:

 {
  currencyObject: { },
  currencyCode: "EUR",
 }

I expect something like:

 {
  currencyObject: "EUR",
  currencyCode: "EUR",
 }

The currency object should be transformed into a JSON string. And since I do not want to transform each response manually, I want to hook into marshalling process and have it done in the background.

I want to add a custom marhaller only for Java.Util.Currency objects, yet even reading up on the docs I am very unsure how to proceed. There are multiple approaches described, and I am not sure which fits my need, or where to start.


I tried creating my own CurrencyJsonProtocol:

package com.foo.api.marshallers

import java.util.Currency

import spray.json.{DefaultJsonProtocol, JsString, JsValue, RootJsonFormat}

object CurrencyJsonProtocol extends DefaultJsonProtocol {

  implicit object CurrencyJsonFormat extends RootJsonFormat[Currency] {
    override def read(json: JsValue): Currency = {
      Currency.getInstance(json.toString)
    }

    override def write(obj: Currency): JsValue = {
      JsString(obj.getCurrencyCode)
    }
  }

}

yet the mere existence of the file breaks my project:

[error] RouteDefinitons.scala:90:16: type mismatch;
[error]  found   : scala.collection.immutable.HashMap[String,java.io.Serializable]
[error]  required: akka.http.scaladsl.marshalling.ToResponseMarshallable
[error]       complete(shouldBeFormated)
[error]                ^
[error] one error found
[error] (Compile / compileIncremental) Compilation failed

and I have no idea why. (It was crashing due to my package name being called marshaller. That completely broke the compilation of the project.

Lette answered 11/4, 2018 at 9:42 Comment(6)
There's some type information that must be available for json conversion to happen. However, using a HashMap makes you loose that type information. That's why I suggested you to use a case class in my answer, that would allow you not to loose the needed type information, at the expense of one more JsonFormat to write.Laroche
Just updated my answerLaroche
@FredericA I have a multitude of response objects, all being case classes and a lot of them have currency objects at different nesting levels and also in sequences. The hash map is just there as an example to showcase the problem. I want the currency objects to be converted in any case class no matter where it appears. Currenlty, I only get {}. From my current understanding it appears as if I have to create a marshaller for each case class which seems redundant and verbose.Lette
You have to create a JsonFormat for each case class, yes. But still, you only need to write JsonFormat once for members like Currency, they will be found through implicit resolution. Writing each JsonFormat separately is considered normal as those represent the public interface of your module/application.Laroche
That said I still don't understand why your Currency gets formatted as {} do you already have a JsonFormat for that?Laroche
@FredericA. No, I don't think I have a JsonFormat for that. Why do I want/need it? The entire marshalling process still feels like black magic to me and I don't understand the reasoning behind it and how to set it up.Lette
I
2

From what I understand, you have all the pieces, you just need to put them together.

Spray json provides support for marshalling common types, such as Int, String, Boolean, List, Map, etc. But it doesn't know how to marshall 'Currency'. And to solve that you have created a custom marshaller for 'Currency' objects. You just need to plug it in the right place. All that you have to do is import the marshaller from your CurrencyJsonProtocol into your Controller like below:

import CurrencyJsonProtocol._

Also make sure you have the below imports as well:

import spray.httpx.SprayJsonSupport._ import spray.json.DefaultJsonProtocol._

And spray-json should automatically pick that up.

To understand how it works, you need to understand about implicits in scala. Although it looks like magic when you come from java-world like me, I can assure you it's not.

Interplanetary answered 19/4, 2018 at 10:15 Comment(0)
L
0

Do you have a JsonFormat[Currency]? If yes, just fix it...

If not, you should follow the doc and create:

  1. as domain model, specific result type to hold the answer e.g. case class CurrencyResult(currency: Currency, code: String)
  2. in trait JsonSupport: a JsonFormat[Currency]
  3. in trait JsonSupport: a JsonFormat[CurrencyResult]

As an alternative to point 1 above, you can probably type your map as: Map[String, JsValue].

Laroche answered 11/4, 2018 at 9:58 Comment(1)
This is not helpful to me as it presupposes some knowledge I don't have. Could you please explicate? Furthermore, I want to act on on any Currency object within any Case Class no matter where in the response it occurs.Lette
O
0

I´m using Spray support for years. Maybe i should try another one. Anyway, in Spray, a custom formatter for a type is simple. An example of the squants.market.currency https://github.com/typelevel/squants

import squants.market.{ Currency, Money }
import squants.market._

 trait MoneyJsonProtocol extends DefaultJsonProtocol {

  implicit object CurJsonFormat extends JsonFormat[Currency] {
    def write(currency: Currency): JsValue = JsString(currency.code)
    def read(value: JsValue): Currency = value match {
      case JsString(currency) =>
        defaultCurrencyMap.get(currency) match {
          case Some(c) => c
          case None => // throw an exception, for example
        }
      case _ => // throw another exception, for example
    }
  }

  implicit def toJavaBigDecimal(b: math.BigDecimal): java.math.BigDecimal = b.underlying

}
Orlop answered 11/4, 2018 at 10:25 Comment(3)
See my updated question, as I tried with a custom JsonProtocol, yet it breaks my project and I don't know why.Lette
that is because your serializer is not in scope. Just import it and it should compile. With Spray you should extend SprayJsonSupport. Note that you should add the dependence libraryDependencies += "com.typesafe.akka" %% "akka-http-spray-json" % "10.1.1"Orlop
I am not entirely sure that's the problem. Did you see my updated question?Lette

© 2022 - 2024 — McMap. All rights reserved.