# Hacking oneOfBody to deal with legacy APIs when converting to tAPIr

**URL:** <https://softwaremill.community/t/hacking-oneofbody-to-deal-with-legacy-apis-when-converting-to-tapir/148>\
**Category:** tapir\
**Created:** [March 6, 2023, 1:33pm UTC](https://softwaremill.community/t/hacking-oneofbody-to-deal-with-legacy-apis-when-converting-to-tapir/148 "2023-03-06T13:33:24Z")\
**Posts on this page:** 3\
**Page:** 1

<div class="post-metadata">

**Author:** ![hochgi](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/hochgi/32/32_2.png) [@hochgi](https://softwaremill.community/u/hochgi)\
**Post date:** [March 6, 2023, 1:33pm UTC](https://softwaremill.community/t/hacking-oneofbody-to-deal-with-legacy-apis-when-converting-to-tapir/148/1 "2023-03-06T13:33:24Z")

</div>

Hi,  
I’m dealing with a legacy application, and we’re refactoring APIs with tapir to provide static contract with schemas, and a better API in general.  
One of the past sins we’re trying to fix, was “joker APIs”.  
Basically, the old API looks something like `GET /api/v1/files/${path}`,  
and the server simply looks at `path`, and if it corresponds to a file, returns it with appropriate content-type.  
e.g: `GET /api/v1/files/dir/subdir/report.html` will return `report.html` if exists with content type `text/html`.

Now, obviously this is a shitty API, looks like a security breach (it’s not really, there’s code to check given path has authorized access for user), no schema, not even a statically known format, etc’…

One interesting thing to note about this, is that most of the files are written in multiple formats.  
e.g: the `report.html` which may contain some table with data, resides next to `report.txt`, which has an ascii-art of the same table and data. Also, `report.csv`, `report.xml`, `report.json`, etc’…

For some paths, there’s also custom code to pull `Array[Byte]` of the content from other places, when no actual file exists.

So, we’re thinking to add multiple APIs for entities like that `report`. Since I don’t really have the `report` entity type (yet; some of the files can be written by another service), I was thinking of doing the following:

`GET /api/v2/entities/Report` but using `oneOfBody` to reply with best format according to `Accept` header.  
With tapir, since I don’t really have the `Report` type, nor any codec that can serialize it, I was thinking of implementing this API as follows:

```plaintext
sealed trait EntityContent {
  def format: CodecFormat
  def asInputStream: InputStream
}

case class TxtFileContent(txt: File) extends EntityContent {
  override def format: CodecFormat = CodecFormat.TextPlain()
  override def asInputStream: InputStream = new FileInputStream(txt)
}

case class HtmlFileContent(html: File) extends EntityContent {
  override def format: CodecFormat = CodecFormat.TextHtml()
  override def asInputStream: InputStream = new FileInputStream(html)
}

case class TextCsv() extends CodecFormat {
  override val mediaType: MediaType = MediaType.TextCsv
}

case class BinaryCsvContent(csv: Array[Byte]) extends EntityContent {
  override def format: CodecFormat = TextCsv()
  override def asInputStream: InputStream = new ByteArrayInputStream(csv)
}

// etc'…

```

and then use some “body” in out of type `List[EntityContent]`.  
Perhaps something like:

```plaintext
// very bad code. decode is benign, encode can throw on empty get
def getBodyFor(cf: CodecFormat): EndpointIO.Body[InputStream, List[ReportContent]] =
  inputStreamBody.map[List[ReportContent]](_ => List.empty) { possibleContents =>
    possibleContents.find(_.format == cf).get.asInputStream
  }

```

So we can:

```plaintext
endpoint
  .in("api" / "v2" / "entities" / "report")
  .get
  .out(oneOfBody(
    getBodyFor(CodecFormat.TextPlain()),
    getBodyFor(CodecFormat.TextHtml()),
    getBodyFor(TextCsv())))

```

But I have several issues with it.  
I don’t always have all possible formats. I also can’t control which formats are available for me.  
So the `.get` may throw, even if I have another format I can serve.  
So this would mean a client has to know which format to ask in `Accept`.

TBH, it is not very different from current situation, where if an API is called for a non-existing file, it will return 404.

So, to my question(s):

1. advice: going forward (into the long future) I do plan to have a hold of the exact entities, and be able to serve al possible formats, so the API I plan also serves a better API evolution (no need to evolve), but: is there a better way? currently safer?
2. Is there a way to control the response when `.get` throws? Can I force it to return 404 with a custom message saying you need to retry with one of the currently existing formats?

Thank you 🙏

---

<div class="post-metadata">

**Author:** ![adamw](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/adamw/32/28_2.png) [@adamw](https://softwaremill.community/u/adamw)\
**Post date:** [March 9, 2023, 8:46am UTC](https://softwaremill.community/t/hacking-oneofbody-to-deal-with-legacy-apis-when-converting-to-tapir/148/2 "2023-03-09T08:46:25Z")

</div>

I think I would try to approach this a bit differently: so that the server logic produces a single `EntityContent` (basing on the `Accept` header content), and then this is properly encoded using `oneOfBody`.

The 404 fallback can then be specified using a `ContentTypeRange.AnyRange`.

The upside is that you’ll get proper documentation and server behavior. The downside is that you’ll have to implement some of the content-type matching manually in the server logic.

The pseudo code might be sth like this: (definitely won’t compile 😉 )

```plaintext
sealed trait EntityContent
case class TxtFileContent(...) extends EntityContent
...
case class ContentNotFound() extends EntityContent

val e = endpoint
  .in(extractFromRequest(_.acceptsContentTypes)).in(...)
  .out(oneOfBody(
    (ContentTypeRange.exact(MediaType.TextPlain), txtBody /* body for TxtFileContent */),
    ...,
    (ContentTypeRange.AnyRange, emptyOutputAs[ContentNotFound].and(statusCode(NotFound))
  ))

```

In the server logic, you can then use `MediaType.matches(ContentTypeRange)` to choose the right representation to return to the server logic. That’s the manual part, but I don’t see a way to skip this.

---

<div class="post-metadata">

**Author:** ![hochgi](https://dub1.discourse-cdn.com/flex005/user_avatar/softwaremill.community/hochgi/32/32_2.png) [@hochgi](https://softwaremill.community/u/hochgi)\
**Post date:** [March 9, 2023, 2:42pm UTC](https://softwaremill.community/t/hacking-oneofbody-to-deal-with-legacy-apis-when-converting-to-tapir/148/3 "2023-03-09T14:42:06Z")

</div>

Thanks, will try this 🙂
