MIME-based versioning via Accept header

Hey there!

Firstly, let me thank you for the great library you have created.

I’m currently trying to make MIME-based versioning (via accept header) work in Tapir,

i.e. I would like to have an endpoint accepting 2 (or more) custom media types (e.g. application/resource-v1+json, application/resource-v2+json) and based on the accept header of the request answering a JSON representing resource of the desired version.

I can make the endpoint accept one custom version (application/resource-v1+json) – with custom codecs for the new media type.

Although, as soon as I define multiple media types in accept header of the endpoint, e.g.:

.in(header(Header.accept(MediaType("application", "resource-v1+json"), MediaType("application", "resource-v2+json"))))

The behavior is as follows. Requests with accept header:

  • application/resource-v1+json - yield 400 Bad request -

Invalid value for: header Accept: application/resource-v1+json, application/resource-v2+json (value mismatch)

  • application/resource-v2+json - yield 400 Bad request -

Invalid value for: header Accept: application/resource-v1+json, application/resource-v2+json (value mismatch)

  • application/resource-v1+json, application/resource-v2+json - yield 200 OK – which, obviously, is not the thing I would like to do.

I’m of undestanding that the endpoint should accept requests with any of the two media types in their accept header.

How can I make the MIME-based versioning work?

Any help is greatly appreciated.

I think there are two solutions to your problem:

  1. a high-level one, using oneOf inputs. Each variant’s body can have a different content type, and this should be used by tapir to implement content negotiation

  2. a low-level one, by manually checking the Accept header. In your current code, you require that as part of the request, there’s an Accept header with the value equal to application/resource-v1+json, application/resource-v2+json - that’s what the fixed-header input does. And that’s also the behavior you are observing. Instead, you want to check that the header matches one of these values. The following input should do the trick:

  header[MediaType](HeaderNames.Accept).mapDecode { headerValue =>
    if (headerValue == MediaType("application", "resource-v1+json") || headerValue == MediaType("application", "resource-v2+json"))
      DecodeResult.Value(headerValue)
    else DecodeResult.Mismatch("resource-v1+json or resource-v2+json", headerValue.toString)
  }(identity)

This input checks that the value is in the allowed range, and reports a mismatch otherwise. In fact, I think adding an enumeration validator will be even better, as you should see the possibilities in the documentation:

  header[MediaType](HeaderNames.Accept).validate(
    Validator.enumeration(
      List(MediaType("application", "resource-v1+json"), MediaType("application", "resource-v2+json")),
      (v: MediaType) => Some(v.toString)
    )
  )

However, if the bodies are different, to have separate documentation for both, you’ll need to use oneOf.