From fe9bc0b7bebfd46eeafc0cb941d71dbc4957c6cd Mon Sep 17 00:00:00 2001 From: GeoffreyXue Date: Thu, 23 Jan 2025 17:06:41 +0000 Subject: [PATCH] feat(146): add oas-specific guidance on mutually exclusive fields --- aep/general/0146/aep.md.j2 | 105 ++++++++++++++++++++++++++++--------- aep/general/0146/aep.yaml | 3 +- 2 files changed, 82 insertions(+), 26 deletions(-) diff --git a/aep/general/0146/aep.md.j2 b/aep/general/0146/aep.md.j2 index 870da47c..395a71e1 100644 --- a/aep/general/0146/aep.md.j2 +++ b/aep/general/0146/aep.md.j2 @@ -1,28 +1,29 @@ -# Generic fields +# Mutually exclusive fields Most fields in any API, whether in a request, a resource, or a custom response, have a specific type or schema. This schema is part of the contract that developers write their code against. -However, occasionally it is appropriate to have a generic or polymorphic field -of some kind that can conform to multiple schemata, or even be entirely -free-form. +However, occasionally it is appropriate to have a mutually exclusive or +polymorphic field of some kind that can conform to multiple schemata, or even +be entirely free-form. ## Guidance -While generic fields are generally rare, a API **may** introduce generic field -where necessary. There are several approaches to this depending on how generic -the field needs to be; in general, APIs **should** attempt to introduce the -"least generic" approach that is able to satisfy the use case. +While mutually exclusive fields are generally rare, a API **may** introduce +mutually exclusive field where necessary. There are several approaches to this +depending on how mutually exclusive the field needs to be; in general, APIs +**should** attempt to introduce the "least mutually exclusive" approach that is +able to satisfy the use case. -For example, an API **should not** use a completely generic field (such as -`google.protobuf.Struct` in protobuf APIs) when the value of the field must -correspond to one of a known number of schemas. Instead, the API **should** use -a [`oneof`](./oneof) to represent the known schemas. +For example, an API **should not** use a completely mutually exclusive field +(such as `google.protobuf.Struct` in protobuf APIs) when the value of the field +must correspond to one of a known number of schemas. Instead, the API +**should** use a [`oneof`](./oneof) to represent the known schemas. -### Generic fields in protobuf APIs +{% tab proto %} -#### Oneof +#### Type Union A `oneof` **may** be used to introduce a type union: the user or API is able to specify one of the fields inside the `oneof`. Additionally, a `oneof` **may** @@ -33,11 +34,11 @@ Because the individual fields in the `oneof` have different keys, a developer can programmatically determine which (if any) of the fields is populated. A `oneof` preserves the largest degree of type safety and semantic meaning for -each option, and APIs **should** generally prefer them over other generic or -polymorphic options when feasible. However, the `oneof` construct is ill-suited -when there is a large (or unlimited) number of potential options, or when there -is a large resource structure that would require a long series of "cascading -oneofs". +each option, and APIs **should** generally prefer them over other mutually +exclusive or polymorphic options when feasible. However, the `oneof` construct +is ill-suited when there is a large (or unlimited) number of potential options, +or when there is a large resource structure that would require a long series of +"cascading oneofs". **Note:** Adding additional possible fields to an existing `oneof` is a non-breaking change, but moving existing fields into or out of a `oneof` is @@ -48,9 +49,9 @@ breaking (it creates a backwards-incompatible change in Go protobuf stubs). Maps **may** be used in situations where many values _of the same type_ are needed, but the keys are unknown or user-determined. -Maps are usually not appropriate for generic fields because the map values all -share a type, but occasionally they are useful. In particular, a map can -sometimes be suited to a situation where many objects of the same type are +Maps are usually not appropriate for mutually exclusive fields because the map +values all share a type, but occasionally they are useful. In particular, a map +can sometimes be suited to a situation where many objects of the same type are needed, with different behavior based on the names of their keys (for example, using keys as environment names). @@ -86,9 +87,63 @@ which is an often-unfamiliar process. Because of this, `Any` **should not** be used unless other options are infeasible. -### Generic fields in OAS APIs - -**Note:** OAS-specific guidance not yet written. +{% tab oas %} + +#### Type Union + +Under the definition of the OpenAPI Specification, [JSONSchema][] keywords for +schema composition (`allOf`, `anyOf`, `oneOf`, `not`) **may** be used to +introduce a type union. The type union produced is equivalent to that of using +`oneof` in protobuf APIs. Due to the schema's complexity, validation for this +schema **should** be handled server-side, and it is fine to not document the +schema explicitly. + +```JSONSchema +{ + "$schema": "https://json-schema.org/draft-04/schema", + "title": "Book", + "type": "object", + "allOf": [ + { + "properties": { + "title": { + "type": "string" + } + } + }, + { + "oneOf": [ + { + "properties": { + "isbn_number": { + "type": "string" + } + }, + "required": ["isbn_number"], + }, + { + "properties": { + "doi": { + "type": "string" + } + }, + "required": ["doi"], + }, + { + "not": { + "anyOf": [ + { "required": ["isbn_number"] }, + { "required": ["doi"] }, + ] + } + } + ] + } + ] +} +``` + +{% endtabs %} [any]: https://github.com/protocolbuffers/protobuf/tree/master/src/google/protobuf/any.proto diff --git a/aep/general/0146/aep.yaml b/aep/general/0146/aep.yaml index 4ec5919e..e91e8ef8 100644 --- a/aep/general/0146/aep.yaml +++ b/aep/general/0146/aep.yaml @@ -1,7 +1,8 @@ --- id: 146 state: approved -slug: generic-fields +slug: mutually-exclusive-fields created: 2024-07-02 +updated: 2025-01-23 placement: category: fields