-
Notifications
You must be signed in to change notification settings - Fork 136
Description
In many places (maybe even all places), I believe the Stripe OpenAPI spec should be using oneOf, but instead uses anyOf. I'll first present a bit of my journey in properly defining anyOf, so if you're not interested, you can skip a bit down to the examples in the Stripe spec.
Neither anyOf or oneOf are defined in OpenAPI v3 itself, so we have to refer to the relevant JSON Schema spec, where we find:
anyOf: "An instance validates successfully against this keyword if it validates successfully against at least one schema defined by this keyword's value."oneOf: "An instance validates successfully against this keyword if it validates successfully against exactly one schema defined by this keyword's value."
The main difference is the "at least one schema" and "exactly one schema" wording. I take that to mean that, for anyOf, an instance can also validate against more than one schema. So for example, if we have an anyOf that looks like this:
anyOf:
- type: object
required:
- foo
properties:
foo:
type: string
- type: object
required:
- bar
properties:
bar:
type: stringThen the following JSON objects (and only the following) would validate:
{ "foo": "abc" }{ "bar": "def" }{ "foo": "abc", "bar": "def" }
Whereas if that was a oneOf instead of anyOf, only the first and second instances would successfully validate. I was still a bit uncertain about this, so I asked Claude to explain the difference between oneOf and allOf (without suggesting what my understanding was), and its answer confirmed my interpretation. I also did some searching around the web that appears to confirm.
So, back to Stripe's spec. One illustrative example (with many bits elided for brevity):
components:
schemas:
card:
properties:
account:
anyOf:
- maxLength: 5000
type: string
- $ref: '#/components/schemas/account'
type: objectNot only is this not correct when considering how the Stripe API actually works, but it's also nonsensical: there is no way you can return a JSON blob that is both a bare string and an object with properties. So this is actually oneOf, not allOf. (This pattern is repeated many, many times in the spec when defining an expandable field that refers either to a resource ID or an expanded representation of that resource.)
I looked for more examples, and every single instance of anyOf I looked at didn't make sense. Here's another (again with many bits elided for brevity):
paths:
/v1/customers:
post:
operationId: PostCustomers
requestBody:
content:
application/x-www-form-urlencoded:
schema:
additionalProperties: false
properties:
address:
anyOf:
- properties:
# a bunch of simply-typed object properties here...
type: object
- enum:
- ''
type: stringThis is again nonsensical, as the address property cannot both be a JSON object with properties, and an empty-string enum variant. It of course could be one of those things, so oneOf is appropriate there instead.
I can't find it now, but I also encountered one where two schemas listed in the anyOf are JSON objects that each have a property with the same name, but with different and incompatible object types, which is also unrepresentable. (And indeed, the Stripe API would never actually try to return something that looks like that.)
I also found some with only a single schema listed under the anyOf; I assume this is done in case future changes introduce more options, but I expect oneOf is actually the correct option for those as well.
Reading through the code of async-stripe's purpose-built OpenAPI parser/generator, I saw that their implementation of anyOf is tailored specifically for the (incorrect) usage in Stripe's spec.
My selfish reason for asking for this to be fixed: my own Rust OpenAPI parser/generator doesn't currently support anyOf, as it's actually fairly tricky to do properly, and especially to express succinctly/user-friendly-ly in a language like Rust. In the meantime I've just s/anyOf/oneOf/ in the spec, and I'm expecting that will work ok, but there are so many instances of anyOf in there, and I don't think I have the context and expertise to audit them all. (I'm using only a small number of the API's endpoints, so this will probably be fine for me regardless.)
(Regarding a Rust implementation of anyOf, consider that if I were to use an enum, each describing a particular valid combination, that means I'd need (2^n)-1 enum variants, where n is the number of schemas in the anyOf. This explodes pretty quickly and is not pleasant to deal with for the user of the generated code. Other approaches, like including all possible properties, with the values all wrapped in Option, mean more difficult work writing/generating code to validate the data. I just built a simple implementation into my generator that uses this latter approach, but without any validation. It... "works", but is pretty sub-optimal. So I've otherwise just kinda ignored the problem until now, given the difficulties.)