2025-04-12

Asking ChatGPT about API design/OpenAPI and stuffs

It’s about API design, but also more.

My problem: I dislike single-endpoint multiple-type payloads (polymorphism), but it is what They are pushing for.

I tried to talk about it with ChatGPT.

Context

First, let’s try to explain few bits of context without revealing too much.

There’s a legacy system, let’s call it TLS, which accepts requests on a socket, very simply, and each request is a XML, like:

<message>
  <service>nameOfTheService</service>
  <param1>…</param1>
  <param2>…</param2>
  <param3>…</param3>
  <complexParam1>
    <this p="1">...</this>
    <something/>

  </complexParam1>

</message>

The services exposed by this TLS are being ported, slowly, to a new system, NS; and NS exposes also new services, of course. This NS is written in Java using Spring Boot.

At the same time, around TLS there is a constellation of other systems, and the interactions between TLS and those other systems happen through a middleware layer.

This middleware layer exposes TLS services as well as other systems services, and itself, in its long life, is made of parts which are marked as legacy, hence doomed to be changed.

In particular, it exposed services using Tuxedo technology, as well as Oracle Service Bus and another ESB.

These technologies did the good job of integrating systems and keeping clients and services in their comfort zone. For instance, old Siebel CRM used to communicate through Tuxedo; so there were Tuxedo servers which received requests for TLS, and made a TCP/IP calls as TLS liked, received the response and responded in the Tuxedo-way. Everyone was talking happily its language.

The point is, all that stuff is marked as obsolete and it’s going to be removed. The middleware layer added a new player, Kong; and also a new approach. In fact, in the old middleware way, there was a habit of implementing some of the business logic in the middleware layer. So that there are Tuxedo servers and webservices1 which aren’t plain and simple adapters of protocol.

All this must go away: the new way includes the use of a common “language”, namely, HTTP with JSON payloads2, and all clients3 have to talk this way4. Moreover, business logic must be pushed down to the clients5, and the middleware must act only as a message-delivery system, which at most can also do orchestration6.

In this chaotic transformation7, one of the first issue we dealt with8 is this: exposing TLS services as HTTP services. So, a new component, let’s call it GWL, was developed. Its duty is to receive HTTP calls with JSON payloads, remap the payload to XML, do the TCP/IP call to TLS, and remap back the XML response in JSON.9

So we have requests like these:

   POST /gwl/service/nameOfTheService
   … headers
   … payload

And now, we need to describe these calls using OpenAPI. It is rather natural to write specs where a fragment like this appears:

paths:
  /gwl/service/nameOfTheService:
    post:


  /gwp/service/anotherService:
    post:

Remember the NS? That was designed as a server already exposting services which can be invoked with HTTP/JSON calls. We tried to make it look RESTful, so you can imagine that we have requests like:

POST /ns/something/user/1234
…

which is some kind of “action”10 on the user identified by 1234. Of course, in this example the OpenAPI spec would contain the path:

  /ns/something/user/{userId}
  …

And other paths like this, with something replaced by something else.

Problem

The problem is that, each and every time we present an OpenAPI spec like that, they frown. Fact is, we realized that they want to diminuish the number of APIs, and they count every path as an API11 — that’s why they are against that (and also against REST).

Their bright12 idea is to have a single endpoint — a single path — and to put everything there. This is almost like the TLS way; but TLS comes from the fog of the past, while this solution is “modern”.13

When you want to model that in OpenAPI, you use a discriminator field. So you end up with a heavely polymorphic API.

Let’s go back to how TLS services are exposed. Do you see it? Indeed, after all, it was already something like that. But the way you describe it, matters, both for maintainability, and how tools handle that. E.g., generators of servers or clients can produce cleaner code if you avoid to describe the API as a single endpoint with lots of different payload types.14

Anyhow, the idea to stuff different APIs on a single path to satisfy their vision of thing, is horrorful to me.

Then I wanted to understand why they like it so. And so I tried to explain the situation to ChatGPT. But first, I asked to help me cheating, that is, use polymorphism where the discriminator is in the path… that would be a different way to describe how things are already!

Context TL;DR

A legacy system offer services that can be invoked sending a XML in a socket; the XML has a tag <service> specifying a service. Business/Middleware guys prefer that these and other APIs are re-implemented on a single endpoint, that is, using polymorphism with a service name as discriminator in the JSON payload.

Instead, we would prefer several endpoints, even parametrized by the service, which comes also more natural in OpenAPI and generated (or hand-written) code, while heavy polymorphism is ugly and technically questionable.

Asking ChatGPT

My Questions are in red, the Answers are in bluish background. Comments in added notes, or not in red nor in bluish background. Redacted.

can the OpenAPI spec describe an API where the discriminator for different payloads is in the path?

OpenAPI can describe different payloads based on the path, but not using discriminator directly — at least not in the way it’s typically used (which is for polymorphism in the request or response body using a field inside the JSON payload).

However, you can achieve similar behavior by defining separate paths for each type, and associating each path with a different schema for the request/response. Here’s how that might look:15

Example scenario

Suppose your API has endpoints like:

POST /api/foo/{type}

Where {type} can be cat, dog, or fish, and each type has a different schema.

Option 1: Explicit paths per type

Define each type explicitly in the path, and assign a different request body schema to each:

paths:
  /api/foo/cat:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Cat'
  /api/foo/dog:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Dog'
  /api/foo/fish:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Fish'

This is the cleanest and most OpenAPI-friendly approach.16


Option 2: One path, use oneOf (less specific, no path-based discriminator)

You can still define:

paths:
  /api/foo/{type}:
    post:
      parameters:
        - name: type
          in: path
          required: true
          schema:
            type: string
            enum: [cat, dog, fish]
      requestBody:
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/Cat'
                - $ref: '#/components/schemas/Dog'
                - $ref: '#/components/schemas/Fish'

But this doesn’t enforce any tight coupling between the path parameter and the schema — OpenAPI won’t validate that {type}=dog implies the Dog schema is used.17

[…]

Then, I wondered. What about responses? Each request can have its own response type!

πŸ“ Notes

  • discriminator in response is handled the same way18: OpenAPI clients and tools like Swagger UI can show the possible variations and validate them accordingly.
  • This setup is very common in polymorphic APIs — especially useful in microservice designs or SDKs that wrap over multiple types.

[…]

how much is really very common? A heavy polimorphism isn’t a design which trumps REST concepts?

Great question — you’re spot on to challenge the idea that heavy polymorphism is “common” or even RESTful. Let’s unpack it.


πŸ” Is polymorphism common in APIs?

In practice: - Light polymorphism (a couple of types with a shared base, like Cat/Dog) is not unusual. - Heavy polymorphism (deep hierarchies, 10+ types, complex discriminator logic) is less common, and usually seen in: - Enterprise APIs modeling complex business domains.19 - Event-based systems, e.g., webhooks where different payloads share a delivery format.20 - SDK-focused APIs, where clean code generation is prioritized over raw REST design.21 - GraphQL-like APIs, or APIs bridging object-oriented backends.22

But it’s not the norm for classic CRUD REST APIs.


🧭 Does polymorphism violate REST principles?

Not exactly — but it can bend them.

REST encourages:

  • Predictability through uniform interfaces.
  • Self-descriptive messages (each message tells you how to understand it).
  • Statelessness, resource orientation, and clear URLs.

Polymorphic payloads: - ✅ Are self-descriptive (via type fields). - ❌ Can blur resource boundaries: /animals becomes a “bag of types” instead of a clear resource. - ❌ May imply behavior (e.g., “if it’s a Dog, run this logic”), which REST prefers to externalize into state transitions.


✅ When is polymorphism a good fit?

  • When you genuinely have a hierarchy of types (not just one-off variations).23
  • When the client needs to know what type it’s receiving to behave differently.
  • When you’re exposing a domain model, especially for internal APIs.24
  • When you’re building APIs that need to generate strongly typed SDKs.25

🚩 When to avoid or rethink it?

  • If you’re just trying to avoid creating multiple endpoints (laziness, not design).26
  • If clients don’t need to distinguish types — just treat them as Animal.
  • If it makes client logic convoluted.27
  • If it’s hiding state transitions that should be made explicit.

[…]


Let me flip the question back: what’s your use case — do you have a polymorphic design in mind, or are you wondering how much to “flatten” things for REST?

the usage case is this: there’s a legacy system […]

Here I try to explain the situation. And then:

[they] want us to make a single path, like

POST /service

and put the service name in the payload, imitating closely the XML. But this is a less clean design, in my opinion, and I have to say that generators (of servers from OpenAPI specs) are rather weak and generate horrorful code… Even if maybe clients has the same complexity with this design, servers (made with spring boot in java) looks very much clearer and cleaner if the entry point to each controller method is determined by the path and http method.

Ahh yes, I completely see where you’re coming from — and you’re 100% right to feel uneasy about it.

What they are asking for is essentially to recreate an RPC dispatcher pattern over HTTP/JSON — which looks like:

POST /service
{
  "service": "SetUserData",
  "id": 1234,
  "name": "Mario",
  "surname": "Rossi"
}

And you end up writing something like:

@PostMapping("/service")
public Response handle(@RequestBody BaseRequest req) {
    switch (req.getServiceName()) {
        case "SetUserData":
            return handleSetUserData((SetUserDataRequest) req);
        case "GetInformation":
            return handleGetInformation((GetInformationRequest) req);
        ...
    }
}

Which quickly devolves into a brittle, hard-to-test, single-endpoint dispatcher. That’s not RESTful, not clean, and doesn’t scale well in terms of validation, code gen, or separation of concerns.


✅ What you’re doing now (e.g., /service/SetUserData) is better

You already refactored into a much cleaner structure:

POST /service/SetUserData  →  Controller method: setUserData()
POST /service/GetInformation →  getInformation()

This:

  • Gives you clean routing.
  • Allows Spring Boot to bind parameters cleanly via DTOs.
  • Enables separate validation, logic, test coverage per service.
  • Is much easier to auto-generate from OpenAPI.
  • Doesn’t make you write a mega-dispatcher method.

Yes — it’s RPCish, but it’s structured RPC.


🧱 Why the single-endpoint approach is appealing to business28

  • It mirrors the legacy format → simpler mental mapping.29
  • It means they only document one endpoint.30
  • Sometimes they think it means fewer deployment changes (spoiler: not true).

But all of this is a superficial win — it makes life easier for the business at the cost of maintainability, clarity, and extensibility on the server side.


😬 Generating code from OpenAPI with discriminators?

You’re also right that: - OpenAPI’s discriminator support is patchy across generators. - Spring Boot generators (like Swagger Codegen, OpenAPI Generator) create monster methods when using oneOf with a discriminator. - Validation annotations and polymorphic deserialization tend to break or get convoluted.


✨ Recommendation: Split paths and map to controllers

Tell business:

“We can mimic the legacy format in payload structure, but to support clean server-side routing, validation, and monitoring, we split the requests per service like /service/SetUserData. The benefit is safer, more testable, and maintainable APIs — and we can still reflect the service if needed in the response or logs.”

You can even keep "service" in the JSON if they really want it:

{
  "service": "SetUserData",
  "id": 1234,
  "name": "Mario",
  "surname": "Rossi"
}

But that’s just a field, not a switch.


✅ TL;DR

Option RESTful Maintainable Tool-friendly Mirrors legacy
/service/SetUserData etc. Medium ✅✅✅ ✅✅✅ Medium
Single /service endpoint ✅✅

Keep what you’re doing. Maybe compromise by: - Keeping service in the payload, - Or documenting all services in one place for business folks.

But avoid the “God endpoint” — that’s how monsters are born 🧟31

the stronger promoter of this approach on the technical side are the guys who are in charge of a middleware system which, afaik, is using a famous API gateway, namely Kong. Is it possible that these guys prefer the “god endpoint” approach to please the business, since the god endpoint is cheaper than handling several endpoints? 32

Ah, now the picture is becoming crystal clear — and yes, you’re likely spot on. The “god endpoint” approach may indeed be promoted more out of operational or political convenience than technical merit.

Let’s unpack what may be going on:


πŸ’Ό The Middleware/Kong Team’s Perspective

If they’re sitting between your API and external consumers33, their concerns often include:

  • Routing simplicity: One endpoint is easier to expose and maintain in the gateway config.
  • Security/policy injection: Easier to apply rate-limits, authentication, and logging if everything hits the same path.
  • Versioning control: One endpoint means less need for multiple plugin configs, route rules, path rewrites, etc.
  • Minimal effort: It avoids creating and managing dozens of Kong routes, plugins, and services.

So… if business says “make it easy to understand, like the old XML,”34 and middleware says “we can route it all through one endpoint,” that’s a double win for them.

But…


🚨 This shifts cost and complexity to you, the backend team35

You get:

  • ❌ A massive dispatcher method.
  • ❌ No native Spring controller routing.
  • ❌ Messy OpenAPI specs (oneOf, discriminator, and broken codegen).
  • ❌ Validation logic that has to be implemented manually.
  • ❌ Difficult testing and traceability per use case.
  • ❌ Logs that don’t clearly show which service failed unless you add custom logging everywhere.

They just shift all the granularity and structure to your code, while keeping their part flat and simple.


πŸ€” Could Kong handle multiple endpoints instead?

Absolutely yes. Kong (and API gateways in general) are made to:

  • Route based on path/method.
  • Version paths (/v1/service/SetUserData, /v2/...).
  • Apply plugins per route or group of routes.
  • Use tags to manage multiple APIs.

The only reason this would be “expensive” is if they lack automation or good ops practices (e.g., scripting Kong config with decK, Terraform, or Kong’s Admin API).

In other words: Kong can do it. But they may not want to, because it means more config, testing, and ownership on their side.


🎯 So your hunch is likely correct:

  • The god endpoint is politically and operationally convenient for them and for business.
  • But it is architecturally and technically worse for the teams maintaining the backend API logic — aka: you.

🧠 Strategy Tip: Shift the conversation

You could say:

“If the gateway can’t support routing multiple services cleanly, we’re forced to reimplement routing logic in the backend — effectively doing what Kong is supposed to handle36. This introduces validation and security risks, and hurts developer productivity. Is there a way we can invest in automating the gateway configs instead?”

That invites collaboration instead of confrontation.37

Later, I wondered how to avoid to replicate service in the response.

what about the response? for most of the service, it is the same “base” struct, but for few cases it is a more complex one. And the service should not be present in the response. But since callers know which service they called with, they should be still able to parse a specific response. So, in the OpenAPI spec, can I make a “meatball” like

  • anyOf:
    • $ref: ‘…/commonResponse’
    • $ref: ‘…/responseOfService40’
    • $ref: ‘…/responseOfService54’ …

where each of the responseOfServiceNN is like

 allOf:
         - $ref: '…/CommonResponse'
         - $ref: '…/specificResponseOfServiceNN'

Yes — you’re thinking about it exactly right. This “meatball” structure (nice metaphor πŸ˜„) is a valid and practical pattern for modeling polymorphic responses in OpenAPI when the discriminator is not in the response, but the client already knows which structure to expect.


✅ The “meatball” response with anyOf and allOf

Your proposed structure:

anyOf:
  - $ref: '.../commonResponse.yaml'
  - $ref: '.../responseOfService40.yaml'
  - $ref: '.../responseOfService54.yaml'

Where responseOfService40.yaml looks like:

allOf:
  - $ref: '.../commonResponse.yaml'
  - $ref: '.../specificResponseOfService40.yaml'

…is a solid approach in OpenAPI 3.0/3.1. You’re basically saying:

The response could be one of these, and we expect the client to know which one based on the call they made.

This avoids needing a discriminator in the response body, which you rightly don’t want to include.


🧠 When to use anyOf + allOf

Use this approach when:

  • The request defines the “type” of operation (service),
  • The response is commonly the same (commonResponse),
  • But for some operations, additional fields are present,
  • You want to reuse a common base, but allow specific extensions.

πŸ‘Ž Alternatives that don’t work well

  • Using discriminator in the response? Nope — bad idea here, since you don’t return one.38
  • Making every response a union of all possible fields? Nope — it would allow invalid responses.

πŸ§ͺ Pro tip: Add a description to each anyOf entry

So tools (like Swagger UI) can show what’s what:

anyOf:
  - $ref: '.../commonResponse.yaml'
    description: "Generic response used by most services"
  - $ref: '.../responseOfService40.yaml'
    description: "Response returned by service 40 (GetUserDetails)"
  ...

✨ Optional improvement

If your server returns JSON with a consistent envelope, like:

{
  "code": 0,
  "description": "All done",
  "details": {
    // specific per service
  }
}

Then you can model it as:

type: object
properties:
  code:
    type: integer
  description:
    type: string
  details:
    anyOf:
      - type: "null"
      - $ref: ".../DetailsForService40.yaml"
      - $ref: ".../DetailsForService54.yaml"

This keeps your response envelope uniform, and pushes polymorphism into details.


✅ TL;DR

  • Yes, anyOf + allOf is perfect here.
  • Clean, valid OpenAPI.
  • Clients will still parse it fine if they know what they called.
  • Server doesn’t need to include a service/discriminator in the response.

  1. Siebel CRM is gone, but those Tuxedo servers were kept. Likely it happened like so: the new CRM system needed that services, but it does not communicate with Tuxedo: it prefers things like SOAP. So, to reduce costs and risks in porting those Tuxedo services, they created SOAP webservices to expose the services of the Tuxedo servers via SOAP, a way which the new CRM could handle. Then, the SOAP webservices make the Tuxedo calls.↩︎

  2. The term REST is often used. It pisses me off, since we are far from being anything which can be described as REST, unless by “REST” we mean indeed just HTTP calls (with JSON payload…). We failed to design the new APIs in a REST fashion. But it’s not just us. Moreover, as you’ll see if you keep reading, there are forces which try to impose design principles which are anti-REST.↩︎

  3. Well, I am not sure that all clients really do or can do that. Let’s say it differently: as far as I can see and the team I am part of is concerned, “all clients” have to do so. With known exceptions, already: of course, when they are dealing with a product (vs a custom system like TLS or NS and others) and this product has its own idea on how it wants to be used… in this case, they must give up imposing their vision.↩︎

  4. I am sure there are queues in the game, too, but so far, TLS and NS hadn’t to deal directly with them.↩︎

  5. This is OK. Underestimating (and sometimes even forgetting) the clients’ maintainers efforts to do so, not so good. This is the game of a company when it uses external companies to do the job: make it cheaper and cheaper.↩︎

  6. But the reality is this: they try hard to avoid also this.↩︎

  7. I won’t explain why it is very much more chaotic than it could be.↩︎

  8. As one of the maintainer of TLS and developer of NS and being these systems part of a specific business area.↩︎

  9. Yes, there are questionable choices to be made, because XML cannot be mapped 1:1 to JSON, and viceversa. But once you have done them, …↩︎

  10. Not very RESTful, again, but at least… it could look like, and it leaves the door open for a more RESTful modeling.↩︎

  11. Let’s joke about it. A parametric path is, indeed, a different path for each possible value of the parameters. So, consider the example of the text: if there are 125000 users, there are, indeed 125000 APIs…↩︎

  12. Sarcasm.↩︎

  13. And somewhere, they still use the word REST — which is then used just as synonym for HTTP with JSON payload, because you surely can’t do REST properly with a single path!↩︎

  14. For TLS it can come easy, because parameters differ according to the service, which is in the payload itself. But for the NS, for example, it is not so. In their mind, we should have added a discriminator field to replicate how TLS did things…↩︎

  15. This is exactly what we have already. But they appear like N paths, so N APIs, and so they think it is not polymoprhic.↩︎

  16. And in fact it is exactly what anyone would do, and exactly what we had already… We could mock them like this: indeed our API is already polymorphic, and the discriminator is in the path. The spec you are looking at, is how OpenAPI expresses polymorphism with a path parameter as discriminator.↩︎

  17. This is an interesting option: it would satisfy them and doesn’t need further development from our side. The week coupling shouldn’t be an issue, because a piece of code can put the discriminator in both the payload (to satisfy the Kong/middleware guys) and the path. Hand-made calls aren’t fool-proof — but who cares?↩︎

  18. So, basically, we would need to modify our code anyway, in order to clone the discriminator in the output. This is an extra effort.↩︎

  19. My case might be Enterprise, but surely we all are not modeling complex business domains. Everything looks trivial and even boring.↩︎

  20. Not the case.↩︎

  21. Not the case.↩︎

  22. Not the case.↩︎

  23. Not the case. No hierarchy of types. Sometimes not even related services! They just happen to be exposed by the same system running on the same host on certain ports, and with certain context in the path.↩︎

  24. Unclear to me; which makes me think: not the case.↩︎

  25. Not the case.↩︎

  26. This is the case! But not our case: it is the middleware/Kong guys’ case!↩︎

  27. For sure, it does.↩︎

  28. Instead of “they”, which sounded too vague, somewheere I mentioned business people, because it seems they are just doing business (like paying a redesign of architectures and APIs at the price of a simple porting…) But later, searching for other answers, I specified that it is not just “business people”.↩︎

  29. I didn’t explain that it is not just “us”: I suppose others face the same problem, even when they are not in charge of legacy systems which have the XML format as I have shown. In fact, I think in most cases the mirroring is with SOAP webservices.↩︎

  30. Doomed to become a very fat documentation…↩︎

  31. I want this in my e-mail signature. Because it is exactly what they are doing or dreaming to do: a God endpoing.↩︎

  32. A long time suspect, confirmed by several clues.↩︎

  33. It is so.↩︎

  34. Again, it is not just us with LTS and its XML payload thrown through a socket. But if you think that a lot of other APIs were SOAP webservices… It still works.↩︎

  35. In fact, we are complaining internally that it seems like those middleware/Kong guys don’t want to do a thing… Once the middleware was a facilitator. Now it’s a complicator, and more and more often there’s a desire to by-pass it.↩︎

  36. This is another thing that emerges more and more often: that their work should be “adsorbed” by the client side, adding an extra layer between the middleware and the providers of services. Indeed, we were already forced to do something like this to handle another silly request — which originated from the same problem: unify APIs, that is, exposing different services on the same path. This is a story for another post!↩︎

  37. It’s not my place to do a proposal like this. I won’t do it. I don’t care. Do they want a mess, technically horrorful solutino? They will have it — the resistence will raise only to avoid that they pay cheap something which costed us a doubled effort.↩︎

  38. Not a strong argument against “oh, well, add the discriminator in the output, too!”.↩︎

No comments:

Post a Comment