< Back to articles

Modelina: Generate your data models from API specification

Introduction

One of the most known abbreviations in software engineering is API (application programming interface). When a program provides an API, it means the program can communicate with other programs, such as web apps, mobile apps, other backend services, and so on.

Although APIs are designed to work with other programs, they are intended for use by humans writing those other programs. To understand how a specific API works and how other programs should interact with it, we create API specifications.

Some software engineers may have encountered situations where they needed to communicate with some outdated programs or, in the worst case, missing API specification. To completely avoid not having specification, I find it valuable to adapt an API-first approach when developing software. This approach involves dedicating more time to API design and more time spent within the planning phase and communicating with stakeholders who provide constant feedback on the API design before any code is written.

Having well-defined API specifications, not only thanks to the API-first approach, in the initial phase of software development offers plenty of advantages. One such advantage, particularly beneficial in statically typed languages like Typescript, is the ability to generate data models from our API specifications. This ensures that the API contract will be easily fulfilled once we begin implementing the API.

Modelina

Modelina, developed by the AsyncAPI Initiative, is a tool designed to simplify the process of generating data models from API specifications. It supports a variety of input formats, such as AsyncAPI, OpenAPI, and JSON Schema documents.

It also provides a CLI, which allows developers to process input files and generate data models easily. However, it's worth noting that the CLI has its limitations, for example you cannot generate data models from OpenAPI. The only way to do this is to use the Modelina library programmatically.

Modelina supports a wide range of input versions. For example, it supports versions 2.0.0 through 2.6.0 for AsyncAPI, Swagger 2.0, OpenAPI 3.0, and OpenAPI 3.1 for OpenAPI, and Draft-4, Draft-6, and Draft 7 for JSON Schema.

One of the most impressive things about Modelina is its extensive language support. It supports generating models in Java, TypeScript, C#, Go, JavaScript, Dart, Rust, Python, Kotlin, C++, PHP, and Scala.

Generating data models for AsyncAPI

Before we step into the practical usage of data model generation with Modeling, I would like to introduce AsyncAPI first.

AsyncAPI is an open-source initiative that focuses on simplifying the process of working with Event-Driven Architectures (EDAs), making it as easy as working with REST APIs. It allows the creation of API specifications for asynchronous APIs. In EDAs, communication is asynchronous, often referred to as a “fire and forget”, with events triggering communication between services. AsyncAPI is designed to be compatible with OpenAPI, and there is a tool called AsyncAPI studio for creating, validating, and previewing AsyncAPI specifications.

Let's dive into a practical example of event-driven architecture. Imagine we have two microservices: an authentication service and a user service. When a user signs in, the authentication service handles the process and sends an event to the user service. The user service then extracts the necessary information from the message payload and performs business logic. This could include updating the user's last sign-in attribute, setting their status, and more.

AsyncAPI includes 2 events – when a user signs in and when a user logs out. There is a different message received for both events:

  • UserSignIn - message includes properties like preferences that contain language and notification settings and status of the user
  • UserLogOut - this message includes only the status property indicating that a user is offline

Code snippets below provide a part of the specification. If you would like to see how AsyncAPI specification looks, check the official documentation.

UserSignIn:
      $id: userSignInMessage
      type: object
      additionalProperties: false
      properties:
        preferences:
          type: object
          properties:
            language:
              type: string
              description: The user's preferred language.
            notifications:
              type: boolean
              description: Whether the user has notifications enabled.
          required:
            - language
            - notifications
        status:
          type: string
          enum: [online]
      required:
        - preferences
        - status
    UserLogOut:
      $id: userLogOutMessage
      type: object
      additionalProperties: false
      properties:
        status:
          type: string
          enum: [offline]
      required:
        - status

Now, we can generate data models directly from our AsyncAPI specification using Modelina AsyncAPI CLI. By specifying the desired output language, the path to our AsyncAPI specification, and the output path, we can create data models specific to use cases. Also, notice the option --tsMarshalling , Modelina adds marshal and unmarshal methods to the generated classes. We are then able to marshal (serialize) data model instances to JSON string and unmarshal (deserialize) JSON string to a data model instance.

npx asyncapi generate models typescript docs/asyncapi.yaml -o=src/async-gen --tsMarshalling

The command generates output that consists of several TypeScript files, each named based on the $id keyword’s value specified in the specification.

> ls src/async-gen
StatusEnum.ts        UserEventEnum.ts     UserLogOutMessage.ts UserMessage.ts       UserPreferences.ts   UserSignInMessage.ts

We can import the required classes from the generated files to create an instance of the UserMessage class. Once the instance is created, we marshal it, converting the instance into a JSON string. This string can then be sent within an event-driven architecture channel and distributed by a message broker. When the event is received in another service, we can unmarshal it back into a UserMessage instance, allowing us to work with the data efficiently.

const userMessage = new UserMessage({
  timestamp: new Date().toISOString(),
  userId: '1',
  message: new UserSignInMessage({
    reservedEvent: UserEventEnum.USER_SIGN_IN,
    preferences: new UserPreferences({
      language: 'en',
      notifications: false,
    }),
    reservedStatus: StatusEnum.ONLINE,
  }),
})

// before publishing the message
const marshalled = userMessage.marshal()

// receiving the message
const unmarshaled = UserMessage.unmarshal(marshalled)

Modelina comes with reserved keywords that can automatically rename some of your fields from the AsyncAPI specification. See, for example, how the status field has been renamed to reservedStatus . There is a possibility to customize property naming constraints when using Modelina’s generator programmatically.

One area that could be improved is the unmarshalling process. Currently, when unmarshalling a JSON string into an instance of UserMessage, the message property is not properly unmarshalled into an instance of UserSignInMessage. Instead, it's assigned as a regular object type. This can cause issues when working with the message property, as it's not recognized as an instance of UserSignInMessage.

Summary

In this post, we have discussed APIs and how Modelina can help us generate data models from the API specification instead of manually creating them. Such tools automating this process can save our time and ensure consistency within our codebases. We have specifically focused on its application within an event-driven architecture using the AsyncAPI specification.

Overall, this post shares my experience with Modelina and encourages other developers to seek similar tools to enhance their projects. Despite numerous benefits, I found that Modelina could not fully address the specific use case I had, primarily due to its inability to unmarshal nested properties based on the instance of a root class. Additionally, renaming fields from the specification during the generation process produced unexpected and sometimes surprising outcomes. However it is actively developed, so let’s see what the future brings.

Patrik Hoffmann
Patrik Hoffmann
Backend DeveloperBesides colleagues and tasks, Patrik likes listening to quality music, eating Vietnamese food, and "Pavlišov" (his hometown schnitzel specialty with sauerkraut and dumplings). He also enjoys sports from A to Z.

Are you interested in working together? Let’s discuss it in person!

Get in touch >