Search code examples
scalaopenapiimplicitopenapi-generatoropenapi-generator-cli

Is there a workaround for this format parameter in Scala?


I am generating the Scala code shown below using the openapi-generator.

import examples.openverse.model.{InlineObject, OAuth2RegistrationSuccessful}
import examples.openverse.core.JsonSupport._
import sttp.client3._
import sttp.model.Method

object AuthTokensApi {

def apply(baseUrl: String = "https://api.openverse.engineering/v1") = new AuthTokensApi(baseUrl)
}

def registerApiOauth2(format: String, data: InlineObject
): Request[Either[ResponseException[String, Exception], OAuth2RegistrationSuccessful], Any] =
    basicRequest
      .method(Method.POST, uri"$baseUrl/auth_tokens/register/?format=${ format }")
      .contentType("application/json")
      .body(data)
.response(asJson[OAuth2RegistrationSuccessful])

Where InlineObject and OAuth2RegistrationSuccessful are simply case classes and JsonSupport is the following:

object JsonSupport extends SttpJson4sApi {
  def enumSerializers: Seq[Serializer[_]] = Seq[Serializer[_]]() :+
    new EnumNameSerializer(AudioReportRequestEnums.Reason) :+
    new EnumNameSerializer(ImageReportRequestEnums.Reason)

  private class EnumNameSerializer[E <: Enumeration: ClassTag](enum: E) extends Serializer[E#Value] {
    import JsonDSL._
    val EnumerationClass: Class[E#Value] = classOf[E#Value]

    def deserialize(implicit format: Formats): PartialFunction[(TypeInfo, JValue), E#Value] = {
      case (t @ TypeInfo(EnumerationClass, _), json) if isValid(json) =>
        json match {
          case JString(value) => enum.withName(value)
          case value => throw new MappingException(s"Can't convert $value to $EnumerationClass")
        }
    }

    private[this] def isValid(json: JValue) = json match {
      case JString(value) if enum.values.exists(_.toString == value) => true
      case _ => false
    }

    def serialize(implicit format: Formats): PartialFunction[Any, JValue] = {
      case i: E#Value => i.toString
      }
    }

  implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all
  implicit val serialization: org.json4s.Serialization = org.json4s.jackson.Serialization
}

To reproduce I simply call the method registerApiOauth2 with the corresponding parameters.

The problem is that the compiler crashes because of the format parameter with the following errors:

No implicit view available from examples.openverse.model.InlineObject => sttp.client3.BasicRequestBody.
      .body(data)
No org.json4s.Formats found. Try to bring an instance of org.json4s.Formats in scope or use the org.json4s.DefaultFormats.
.response(asJson[OAuth2RegistrationSuccessful])

The problems are resolved when I change format to any other parameter, such as frmt. However, this cannot be done because the generated query parameter must be called as such. This is the first time I came across such a problem and was hoping that there is a workaround. My hunch is that the issue stems from the JsonSupport object.

Link to Scastie with an MCVE: https://scastie.scala-lang.org/oDmYLP8MQOCMqUYEwhJnDg


Solution

  • I managed to reproduce. I added import JsonSupport._ into the class AuthTokensApi.

    With format https://scastie.scala-lang.org/DmytroMitin/mLtRiWE7SQKySehLJykLAQ doesn't compile.

    With frmt https://scastie.scala-lang.org/DmytroMitin/mLtRiWE7SQKySehLJykLAQ/2 compiles.

    The behavior is understandable. The parameter format of method registerApiOauth2

    def registerApiOauth2(format: String, data: InlineObject)...
    

    hides by name the implicit format defined inside JsonSupport

    implicit val format: Formats = DefaultFormats ++ enumSerializers ++ DateSerializers.all
    

    when an implicit is resoled here

    def registerApiOauth2(format: String, data: InlineObject)... = {
      ...
    
      .body(data)(json4sBodySerializer(... /* HERE! */ ..., .....))
    
      ...
    }
    

    So try to rename either former or latter. If you can't rename any of them then resolve implicits manually and refer the implicit as JsonSupport.format, not just format

    basicRequest
      .method(Method.POST, uri"$baseUrl/auth_tokens/register/?format=${format}")
      .contentType("application/json")
      .body(data)(json4sBodySerializer(JsonSupport.format, serialization))
      .response(asJson[OAuth2RegistrationSuccessful](
        implicitly[Manifest[OAuth2RegistrationSuccessful]],
        JsonSupport.format,
        serialization
      ))
    

    You can read about hiding implicits by name more:

    NullPointerException on implicit resolution

    Extending an object with a trait which needs implicit member

    https://github.com/scala/bug/issues/7788

    Simpler example with the same behavior: the code

    implicit val i: Int = 1
    def m()(implicit x: Int) = ???
    def m1()(i: String) = {
      m()
    }
    

    doesn't compile while

    implicit val i1: Int = 1
    def m()(implicit x: Int) = ???
    def m1()(i: String) = {
      m()
    }
    

    and

    implicit val i: Int = 1
    def m()(implicit x: Int) = ???
    def m1()(i1: String) = {
      m()
    }
    

    compile.