1

I am currently working on a Scala application which utilizes Spring-Boot and Swagger to send & receive REST-calls.

Swagger and Spring-Boot are pure Java-projects and have limited compatibility with Scala, but I seem to have found a workaround regarding the problem.

Since Spring-Boot and Swagger are handling requests as Java objects (which needs setters & getters to work), I'll have to treat the request as a Java object and convert the request to later. This is a very simplified version of what I did:


case class ParamsAsJava(includeMovies: java.lang.Boolean = java.lang.Boolean.FALSE, includeTvShows: java.lang.Boolean = java.lang.Boolean.FALSE) {

  def toScala(): Params = {
    Params(
      includeMovies = convertToScala(includeMovies),
      includeTvShows = convertToScala(includeTvShows)
    )
  }

  private def convertToScala(test: java.lang.Boolean): Boolean
  = if (test == null) false else test.booleanValue

}

case class Params(includeMovies: Boolean = false, includeTvShows: Boolean = false)

object Application extends App {

  val test1 = ParamsAsJava(java.lang.Boolean.FALSE, java.lang.Boolean.TRUE).toScala
  val test2 = ParamsAsJava(java.lang.Boolean.TRUE, java.lang.Boolean.TRUE).toScala
  val test3 = ParamsAsJava().toScala
  val test4 = ParamsAsJava(null, null).toScala
  val test5 = ParamsAsJava(null, java.lang.Boolean.TRUE).toScala

  println(s"Test 1 = $test1")
  println(s"Test 2 = $test2")
  println(s"Test 3 = $test3")
  println(s"Test 4 = $test4")
  println(s"Test 5 = $test5")
}

OUTPUT

Test 1 = Params(false,true)

Test 2 = Params(true,true)

Test 3 = Params(false,false)

Test 4 = Params(false,false)

Test 5 = Params(false,true)


OK... my question is:

Is there an easier & more readable way of achieving this? Do I have to call ParamsAsJava.toScala each time or is there some awesome Scala way of doing this?

Mr.Turtle
  • 2,950
  • 6
  • 28
  • 46
  • 3
    Spring can decode or encode pure scala data classes as well. You simply need to inject `ScalaObjectMapper` that supports scala data classes in `WebMvcConfigurerAdapter` - https://github.com/FasterXML/jackson-module-scala – prayagupa Sep 30 '18 at 20:24
  • 1
    The top-voted answer seems to provide an idiomatic Spring solution, which is focused primarily on Spring, not so much on Scala. Maybe that should be reflected in the tags? – Andrey Tyukin Oct 01 '18 at 12:26
  • This challenge is not about the ScalaObjectMapper, nor the JacksonObjectMapper, its about the @RequestParam' from the Swagger API that can't deal with objects being 'null' (I've tried Scala Option, doesnt work). The solution provided in my question is purely a workaround that I have made because I couldn't find another solution online. I have a Java-background and I'm wondering if there is a better way of doing this in Scala **OR** if there is some other magic I haven't found yet. Thanks! – Mr.Turtle Oct 03 '18 at 17:50

3 Answers3

3

By default Spring uses jackson-mapper to decode JSON request body to Java classes and encode Java classes to JSON response.

Alternatively you can tell Spring to use ScalaObjectMapper which works well for scala data classes.

<dependency>
    <groupId>com.fasterxml.jackson.module</groupId>
    <artifactId>jackson-module-scala_2.12</artifactId>
    <version>2.9.7</version>
</dependency>

Then configure Spring to use ScalaObjectMapper in WebMvcConfigurerAdapter. Example;

import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper
import scala.collection.JavaConverters._

@EnableWebMvc
@Configuration
@PropertySource(Array("classpath:config/default.properties"))
@ComponentScan(Array(
  "com.prayagupd"
))
@Order(HIGHEST_PRECEDENCE)
class MyAppConfig extends WebMvcConfigurerAdapter {

  override def extendMessageConverters(converters: java.util.List[HttpMessageConverter[_]]): Unit = {
    val encodeDecoderOpt = converters.asScala.collectFirst { case p: MappingJackson2HttpMessageConverter => p }

    val jacksonConverter = encodeDecoderOpt.getOrElse(new MappingJackson2HttpMessageConverter)
    jacksonConverter.setObjectMapper(Config.objectMapper)

    if (encodeDecoderOpt.isEmpty) // because converters is mutable
       converters.add(jacksonConverter)
  }
}


object Config {

  @Bean
  def objectMapper: ObjectMapper = {
    new ObjectMapper() with ScalaObjectMapper
  }.registerModule(DefaultScalaModule)
}

That should be it, now you can use pure scala data classes from Spring endpoint definition. Example

@RestController
@CrossOrigin(origins = Array("*"))
@RequestMapping(value = Array("/"))
class MyController @Autowired()(implicit val jacksonObjectMapper: ObjectMapper) {
  import MyController._

  @Async
  @RequestMapping(value = Array("/talk"), method = Array(RequestMethod.POST), consumes = Array(MediaType.APPLICATION_JSON_VALUE), produces = Array(MediaType.APPLICATION_JSON_VALUE))
  def talk(@RequestBody request: MyRequest): MyResponse = {
       // goes here
   }
}

//expose these in client jar if you want
object MyController {
  final case class MyRequest(includeMovies: Boolean, includeTvShows: Boolean, somethingElse: List[String])

  final case class MyResponse(whatever: String)
}
prayagupa
  • 30,204
  • 14
  • 155
  • 192
  • Hello, thanks for replying. This is not a request body, but request params. I need to do it this way because the application needs to be compatible with other applications (outside my control). The problem is not with Jackson.. jackson is working great :-) The problem is with Swagger (which I have to use to document the API). `Field error in object 'Params' on field 'includeMovies': rejected value [null]; codes [typeMismatch.params.includeMovies]; ` I have tried different approaches and it seems like I have to treat everything as Java-values when they arrive as requestParams in Swagger – Mr.Turtle Oct 02 '18 at 08:40
  • I see. I myself remember using java for request params. I will respond once I get time, let me know if this helps - [Spring RequestParam formatter for Scala.Option](https://stackoverflow.com/a/39155809/432903). – prayagupa Oct 03 '18 at 03:04
2
  1. You don't need to write out java.lang.blah every time in

    ParamsAsJava(java.lang.Boolean.FALSE, java.lang.Boolean.TRUE)
    

    Just use

    ParamsAsJava(false, true)
    

    instead. Autoboxing hasn't gone anywhere.

  2. to get rid of toScala, define an implicit conversion in Params companion object:

    object Params {
      implicit def params_j2s(p: ParamsAsJava): Params = p.toScala()
    }
    

    now you can write:

    val test1: Params = ParamsAsJava(true, false)
    

    and, of course, if you don't define this variable in vacuum, but pass it to a method instead, the right type will be inferred automatically, and the object will be converted implicitly.

  3. No need to use parens () in def toScala(), the method has no side effects.

Andrey Tyukin
  • 43,673
  • 4
  • 57
  • 93
2

I used scala.Boolean box and unbox methods.

Karthik P
  • 481
  • 5
  • 9