Applying Polymorphism using OpenAPI
- 2024/12/11
- ブログ
- Applying Polymorphism using OpenAPI はコメントを受け付けていません
Have you ever encountered a situation where you need to handle multiple types of objects using a single variable? While this might seem rare in traditional systems, it’s a common challenge in dynamic field systems that we must address. In this post, I would like to show you how OpenAPI can be leveraged to solve this problem effectively.
1. What is Polymorphism?
The term “Polymorphism” is derived from the Greek words “poly”, meaning “many”; and “morphism”, meaning “form”, or it is likewise the state of being a shape or a structure in Math. In programming, Polymorphism is one of four core concepts in Object Oriented Programming languages, which refers to the ability of an object to take on multiple forms or behave differently depending on the context.
Typically, a variable with polymorphic properties is defined through an interface or a parent class, such as Object
in Java. This allows the variable to be treated as different types at runtime, adapting its behavior according to the specific object it references in real-time processing.
2. Case Study
Assume that I have a form designed to receive many types of data such as text, a number and an array of items. How can we use OpenAPI to generate these data types?
From OpenAPI 3.0, they supports for polymorphic by using oneOf or anyOf keywords.
a. Without Discriminator
... paths: /inquiry/register: post: summary: Register based on contact form information operationId: register description: | Data can be 1. phone number 2. home address 3. list of non-educational certificates is an array 4. education history is an array of school's name, phones, certificates requestBody: content: application/json: schema: $ref: '#/components/schemas/RegisterForm' responses: '202': description: OK content: application/json: schema: $ref: '#/components/schemas/DataResponse' components: schemas: RegisterForm: type: array description: register form items: $ref: '#/components/schemas/RegisterFormItems' RegisterFormItems: type: object description: form elements required: - fieldName - value properties: fieldName: description: form field type: string value: description: input value type: object oneOf: - type: number description: in case of phone number, grade, etc - type: string description: | school name, certificate's name, full name, etc - $ref: '#/components/schemas/CertificateForm' CertificateForm: type: array description: certificate form items: $ref: '#/components/schemas/CertificateItems' CertificateItems: type: object description: certificate form information required: - certificateName - schoolName - expiredDate properties: certificateName: description: certificate's name type: string schoolName: description: where to get this certificate type: string expiredDate: description: until which year will it is valid type: integer DataResponse: type: object description: data response required: - data properties: data: description: ok type: string
After generation, we will have an interface to present dynamic values as follows:
In this case, unfortunately, we cannot directly map the value for each type and will need to prepare a few steps.
Step 1: Create a Class for Primitive Values
In Java, primitive data types (such as String
, Integer
, etc.) and some non-primitive data types (such as arrays) cannot directly implement an interface. Therefore, we need to create a wrapper object like this.
public class NumberWrapper implements RegisterFormValue, NestedItemsInnerValue { private Number value; @JsonValue public Number getValue() { return value; } }
Step 2: Create a Deserialization Function
public class ValueDeserializer extends JsonDeserializer<RegisterFormItemsValue> { @Override public RegisterFormItemsValue deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { JsonNode node = jsonParser.getCodec().readTree(jsonParser); if (node.isTextual()) { return new StringWrapper(node.asText()); } if (node.isNumber()) { return new NumberWrapper(node.numberValue()); } if (node.isArray()) { // handle certificate's array and nested array List<CertificateItems> certificateItemsList = new ArrayList<>(); for (JsonNode element : node) { CertificateItems option = new CertificateItems( element.get("certificateName").asText(), element.get("schoolName").asText(), element.has("expiredDate") ? element.get("expiredDate").asInt() : null); certificateItemsList.add(option); } return new ArrayWrapper(certificateItemsList); } return new StringWrapper(node.asText()); } }
Step 3: Attach this function to the Interface
After completing these steps, we can test with this kind of JSON data.
b. With Discriminator
Starting from OpenAPI 3.0, it supports the discriminator/propertyName keyword, which is used in conjunction with the oneOf or anyOf keyword in order to point to a property in the object that indicates the type of data it represents. This allows the system to distinguish between different object types more easily, without the need for additional mapping functions.
Compared to the previous one, the changes we need to make will look like this:
components: schemas: RegisterForm: type: array description: register form items: $ref: '#/components/schemas/RegisterFormItems' RegisterFormItems: type: object description: form elements required: - fieldName - value properties: fieldName: description: form field type: string value: description: input value type: object discriminator: propertyName: objectType oneOf: - $ref: '#/components/schemas/NumberItems' - $ref: '#/components/schemas/StringItems' - $ref: '#/components/schemas/CertificateForm' StringItems: type: object description: | school name, certificate's name, full name, etc properties: objectType: type: string value: type: string NumberItems: type: object description: in case of phone number, grade, etc properties: objectType: type: string value: type: number CertificateForm: type: object description: list of certifications properties: objectType: type: string value: type: array description: certificate form items: $ref: '#/components/schemas/CertificateItems' CertificateItems: type: object description: certificate form information required: - certificateName - schoolName - expiredDate properties: certificateName: description: certificate's name type: string schoolName: description: where to get this certificate type: string expiredDate: description: until which year will it is valid type: integer
The rule is that for each type of value, we need to add a property name (in this case, objectType
) to indicate to the system the type of value it belongs to. Instead of writing a new deserializer class, OpenAPI will leverage JsonTypeInfo
to help us convert the value in different ways.
Although, as far as I know, OpenAPI supports only JsonTypeInfo.Id.Name
, if you implement a class yourself, there are several other ways that JsonTypeInfo
can specify the type of a value, such as using the Java class name or by writing a custom mapping. For more information, you can read here.
Now, let’s test the result.
3. Summary
All roads lead to Rome, right? However, taking advantage of OpenAPI can help us save time by eliminating the need to generate extra classes. So, why not give it a try sometime? I hope this post is helpful to you.
カテゴリー: