Open czechboy0 opened 1 year ago
@simonjbeaumont to gather some data.
I decided to use the following snippet of OpenAPI YAML for this research:
components:
schemas:
Badger:
type: object
properties:
b:
type: boolean
Mushroom:
type: object
properties:
m:
type: integer
Snake:
type: object
properties:
s:
type: string
MyAnyOf:
anyOf: &schemas
- $ref: '#/components/schemas/Badger'
- $ref: '#/components/schemas/Mushroom'
- $ref: '#/components/schemas/Snake'
- type: string
- type: string
pattern: '\d'
- type: string
enum:
- foo
- type: integer
- type: object
- type: object
properties:
foo:
type: string
- type: array
items:
type: integer
MyAllOf:
allOf: *schemas
MyOneOf:
oneOf: *schemas
I'll use the following set of generators initially:
openapi-generator -g swift
openapi-generator -g go
openapi-generator -g rust
openapi-typescript
openapi-typescript-codegen
I've tried to capture just enough of the output for each generator to draw themes about the question at hand.
openapi-generator -g swift
public struct MyAnyOf: Codable, JSONEncodable, Hashable {
public var b: Bool?
public var m: Int?
public var s: String?
public var foo: String?
}
public struct MyAllOf: Codable, JSONEncodable, Hashable {
public var b: Bool?
public var m: Int?
public var s: String?
public var foo: String?
}
public enum MyOneOf: Codable, JSONEncodable, Hashable {
case typeAnyCodable(AnyCodable)
case typeBadger(Badger)
case typeInt(Int)
case typeMushroom(Mushroom)
case typeMyAnyOfAnyOf(MyAnyOfAnyOf)
case typeSnake(Snake)
case typeString(String)
case type[Int]([Int])
}
openapi-generator -g go
type MyAnyOf struct {
Badger *Badger
Mushroom *Mushroom
MyAnyOfAnyOf *MyAnyOfAnyOf
Snake *Snake
[]int32 *[]int32
int32 *int32
map[string]interface{} *map[string]interface{}
string *string
}
type MyAllOf struct {
B *bool `json:"b,omitempty"`
M *int32 `json:"m,omitempty"`
S *string `json:"s,omitempty"`
Foo *string `json:"foo,omitempty"`
}
type MyOneOf struct {
Badger *Badger
Mushroom *Mushroom
MyAnyOfAnyOf *MyAnyOfAnyOf
Snake *Snake
ArrayOfInt32 *[]int32
Int32 *int32
MapmapOfStringinterface{} *map[string]interface{}
String *string
}
openapi-generator -g rust
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MyAnyOf {
#[serde(rename = "b", skip_serializing_if = "Option::is_none")]
pub b: Option<bool>,
#[serde(rename = "m", skip_serializing_if = "Option::is_none")]
pub m: Option<i32>,
#[serde(rename = "s", skip_serializing_if = "Option::is_none")]
pub s: Option<String>,
#[serde(rename = "foo", skip_serializing_if = "Option::is_none")]
pub foo: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MyAllOf {
#[serde(rename = "b", skip_serializing_if = "Option::is_none")]
pub b: Option<bool>,
#[serde(rename = "m", skip_serializing_if = "Option::is_none")]
pub m: Option<i32>,
#[serde(rename = "s", skip_serializing_if = "Option::is_none")]
pub s: Option<String>,
#[serde(rename = "foo", skip_serializing_if = "Option::is_none")]
pub foo: Option<String>,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct MyOneOf {
#[serde(rename = "b", skip_serializing_if = "Option::is_none")]
pub b: Option<bool>,
#[serde(rename = "m", skip_serializing_if = "Option::is_none")]
pub m: Option<i32>,
#[serde(rename = "s", skip_serializing_if = "Option::is_none")]
pub s: Option<String>,
#[serde(rename = "foo", skip_serializing_if = "Option::is_none")]
pub foo: Option<String>,
}
openapi-typescript
export interface components {
schemas: {
Badger: {
b?: boolean;
};
Mushroom: {
m?: number;
};
Snake: {
s?: string;
};
MyAnyOf: components["schemas"]["Badger"] | components["schemas"]["Mushroom"] | components["schemas"]["Snake"] | string | "foo" | number | Record<string, never> | {
foo?: string;
} | number[];
MyAllOf: components["schemas"]["Badger"] & components["schemas"]["Mushroom"] & components["schemas"]["Snake"] & string & string & "foo" & number & Record<string, never> & {
foo?: string;
} & number[];
MyOneOf: components["schemas"]["Badger"] | components["schemas"]["Mushroom"] | components["schemas"]["Snake"] | string | "foo" | number | Record<string, never> | {
foo?: string;
} | number[];
};
responses: never;
parameters: never;
requestBodies: never;
headers: never;
pathItems: never;
}
openapi-typescript-codegen
export type MyAnyOf = (Badger | Mushroom | Snake | string | 'foo' | number | Record<string, any> | {
foo?: string;
} | Array<number>);
export type MyAllOf = (Badger & Mushroom & Snake & string & 'foo' & number & Record<string, any> & {
foo?: string;
} & Array<number>);
export type MyOneOf = (Badger | Mushroom | Snake | string | 'foo' | number | Record<string, any> | {
foo?: string;
} | Array<number>);
The above outputs show different strategies for handling schema composition, including flattened types, union types, variant types, etc.
However, none of them seem to need to make use of an index-based naming, e.g.
case1
, case2
, etc.
Yeah if constrained to just references to objects as subschemas, those can be handled pretty well, because they have a global name.
But all of these aggregate types (except for oneOfs with a discriminator) can have any schema as a subschemas, and those types can be inlined (in which case you don't have a global name) and don't even have to be unique (you can have two string schemas that only differ by their regex constraint).
Curious to see the output for the following additional subschemas:
- type: string
- type: string
pattern: '\d'
- type: string
enum:
- foo
- type: integer
- type: object
- type: object
properties:
foo:
type: string
- type: array
- type: array
items:
type: integer
I also suspect that variadic generics in Swift could help us here, but all this was designed before they were available.
Those set of subschemas won't work for all of the schema composition methods, because they cannot be used in conjunction in some cases in an OAS-compliant way. However, I threw them into the generators and updated the comment with the findings.
Still no index-based type names.
Sure, in the case of allOf and anyOf, there are some mutually incompatible schemas, but the oneOf should be able to support all 10 schemas. From the above, it seems none of the generators were able to generate more than 8, and some stopped at 7.
And getting all the way to 10 is where they'd have to choose either an order-based name, or create a unique name from the contents, such as string_with_pattern_backslash_d_minItems_10_...
etc. The order-based name is a lot simpler to implement, but the content-based name would also work.
Would love to see a generator that can actually handle all 10 cases, and which method of naming they chose.
but the oneOf should be able to support all 10 schemas.
I don't think that's true. For example you suggested these:
- type: string
- type: string
pattern: '\d'
- type: string
enum:
- foo
which cannot work because it needs to satisfy (exactly one)^1. But the first is always satisfied, which is why I think some of the generators skipped it (there was some diagnostic output to this effect too).
Good catch, let me amend to:
- type: string
pattern: '\s'
- type: string
pattern: '\d'
Revisit server1/server2 and case1/case2 and value1/value2 naming in Servers, oneOf, allOf/anyOf.
We don't like the names, we should derive better unique names, e.g. based on:
We just have to keep in mind that in many cases (e.g. inline schemas), we don't have a unique name to use, and might need to derive one - there we need to watch out for potential naming conflicts.