Mike Slinn
Mike Slinn

Reputation: 8403

Akka-http-json "Unsupported Content-Type, supported: application/json"

I'm having trouble using a custom JSON marshaller/unmarshaller. This much works fine:

trait EWorksJsonSupport extends SprayJsonSupport with DefaultJsonProtocol {
  implicit object IndividualJsonFormat extends RootJsonFormat[Individual] {
    def write(individual: Individual) = JsObject(
      // blah blah blah
    )

    def read(value: JsValue): Individual = {
      // blah blah blah
    }
}

The problem is that Unsupported Content-Type, supported: application/json is returned as shown below:

import akka.http.scaladsl.model.ContentTypes._
import akka.http.scaladsl.model.HttpEntity
import akka.http.scaladsl.testkit.ScalatestRouteTest
import akka.http.scaladsl.unmarshalling._
import eworks.model.immutableModel.SpeciesAll
import eworks.model.mutableModel.{Individual, Individuals, VirtualWorld}
import eworks.model.{Fixtures, LoadableModel, SpeciesDefaultLike}
import org.junit.runner.RunWith
import org.scalatest.Matchers._
import org.scalatest._
import org.scalatest.junit.JUnitRunner
import spray.json._

@RunWith(classOf[JUnitRunner])
class TestRest extends WordSpec with SpeciesDefaultLike with LoadableModel with ScalatestRouteTest with Fixtures with EWorksJsonSupport {    
  "EWorksJsonSupport" should {
    "work for Individuals" in {
      val jsObject: JsValue = harry.toJson
      val entity = HttpEntity(`application/json`, jsObject.toString)

      Post("/addIndividual", entity) ~> new RestHttp()(speciesDefaults).route ~> check {
        handled === true
        contentType === `application/json`
        status.intValue === 200

        val individual1 = Unmarshal(response.entity).to[Individual] 
        // ErrorFuture(akka.http.scaladsl.unmarshalling.Unmarshaller$UnsupportedContentTypeException: Unsupported Content-Type, supported: application/json)
        val individual2 = responseAs[Individual]
        responseAs[Individual] shouldBe harry
      }
    }
  }
}

Upvotes: 2

Views: 5923

Answers (3)

Thomas Luechtefeld
Thomas Luechtefeld

Reputation: 1456

If you can't change the content type you could:

    val stringR : String = Await.result(Unmarshal(r).to[String],Duration.Inf)
    val ind : Individual = Unmarshal(stringR).to[Individual]

Upvotes: 2

Mike Slinn
Mike Slinn

Reputation: 8403

The key to the solution is to call complete with the desired ContentType. Here is a method I wrote that provides an HttpResponse with Content-Type application/json along with the desired content, computed when block is evaluated:

@inline def wrap(block: => JsValue): StandardRoute =
  complete(
    try {
      HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, success(block)))
    } catch {
      case e: Exception =>
        HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, error(e.getMessage)))
    }
  )

I made a trait to encapsulate this handy utility method:

import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpHeader, HttpResponse}
import akka.http.scaladsl.server.{Directives, MediaTypeNegotiator, Route, StandardRoute, UnsupportedRequestContentTypeRejection}
import akka.http.scaladsl.unmarshalling._
import spray.json._
import scala.collection.immutable.Seq

trait RestHttpSupport extends Directives {
  @inline def error  (msg: String): String = JsObject("error"   -> JsString(msg)).prettyPrint
  @inline def success(msg: String): String = JsObject("success" -> JsString(msg)).prettyPrint

  @inline def error  (msg: JsValue): String = JsObject("error"   -> msg).prettyPrint
  @inline def success(msg: JsValue): String = JsObject("success" -> msg).prettyPrint

  @inline def wrap(block: => JsValue): StandardRoute =
    complete(
      try {
        HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, success(block)))
      } catch {
        case e: Exception =>
          HttpResponse(entity = HttpEntity(ContentTypes.`application/json`, error(e.getMessage)))
      }
    )

  @inline def completeAsJson[T](requestHeaders: Seq[HttpHeader])
                               (body: T => StandardRoute)
                               (implicit um: FromRequestUnmarshaller[T]): Route = {
    import akka.http.scaladsl.model.MediaTypes.`application/json`
    if (new MediaTypeNegotiator(requestHeaders).isAccepted(`application/json`)) {
      entity(as[T]) { body }
    } else {
      reject(UnsupportedRequestContentTypeRejection(Set(`application/json`)))
    }
  }

  @inline def postAsJson[T](body: T => StandardRoute)
                   (implicit um: FromRequestUnmarshaller[T]): Route = {
    (post & extract(_.request.headers)) { requestHeaders =>
      completeAsJson[T](requestHeaders) { body }
    }
  }
}

One the trait is mixed in, and assuming that implicit serializers built from SprayJsonSupport with DefaultJsonProtocol are in scope, an Akka HTTP path can be defined using the wrap method. All of this code is taken from EmpathyWorks™ (which is not open source):

path("definedEvents") {
  get { wrap(allDefinedEvents.toJson) }
} ~
path("listIndividuals") {
  get { wrap(individuals.toJson) }
} ~
path("listSpecies") {
  get { wrap(speciesAll.toJson) }
} ~
path("listSpeciesNames") {
  get { wrap(speciesAll.collection.map(_.name).toJson) }
}

Upvotes: 0

Federico Pellegatta
Federico Pellegatta

Reputation: 4017

The HttpResponse response you get from the new RestHttp()(speciesDefaults).route router by posting your entity to /addIndividual (as logged, see below) has text/plain as content-type, you should fix that. Also its content does not look like valid JSON (see below).

Response was:

HttpResponse(
    200 OK,
    List(),
    HttpEntity.Strict(
        text/plain; charset=UTF-8, 
        Individual added: harry is a human; (unborn); lifeStage 'adult'
    ), HttpProtocol(HTTP/1.1)
)

Upvotes: 1

Related Questions