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.

関連記事

カテゴリー:

ブログ

情シス求人

  1. チームメンバーで作字やってみた#1

ページ上部へ戻る