One design goal I’ve been having in mind with frontend code for airavata-django-portal is to keep the “business logic” separate from the UI code, especially to keep it separate from any UI framework code. I got this idea from another blog post that I can’t quite find at the moment but the idea is to keep the core application logic separate from the UI framework code, which makes it easier to switch if needed to a different UI framework. It’s also just a good separation of concerns: there’s no need to tie up pure domain logic with Vue or React or Angular specific framework code.
For the most part, this common application logic code is made up of Models and Services. This post focuses on Models and how to instantiate them from REST API responses. Models are client-side representations of server-side models, so one responsibility of model classes is to handle deserialization and serialization of API responses. Another responsibility of models is to implement validation logic. In general any domain specific behavior should be implemented by these model classes.
So one problem is how to efficiently map from a JSON response to properties on these model classes? I’ve been working on that this week. For the most part JSON.parse
takes care of the simpler aspects of deserialization: strings, numbers, and booleans are all converted to native data types. But there remain a few additional considerations:
- Dates aren’t encoded as such in JSON and require special handling. The approach I’ve been taking is to encode dates as strings in ISO-8601 format, in the UTC timezone.
- Mapping nested data structures to nested model classes.
- Handling lists of simple data values or nested model instances.
There are potentially other considerations like mapping a property with a certain name in the response to a differently named property on the model, but we’re taking the approach of keeping the property names the same on the responses and model classes.
I created a BaseModel.js that has a constructor that takes two arguments. The first argument is an array of metadata that defines all of the “fields” or properties of the model class. The second argument is optional and is the data, which would typically be an API response.
There are two ways to define a field. The first way is to just specify the name of the field. In this case no special conversion logic is performed: if the field’s value is a string then that is the type of the field, etc. The second way to define a field is to specify not only the name of the field but also its type and optionally whether it is a list and what default value it should have if there is no value for this field in the data.
The implementation of the BaseModel constructor is fairly straightforward. Here’s an example of how the BaseModel class would be used:
import BaseModel from './BaseModel' import InputDataObjectType from './InputDataObjectType' import OutputDataObjectType from './OutputDataTypeObject' const FIELDS = [ 'applicationInterfaceId', 'applicationName', 'applicationDescription', { name: 'applicationModules', type: 'string', list: true, }, { name: 'applicationInputs', type: InputDataObjectType, list: true, }, { name: 'applicationOutputs', type: OutputDataObjectType, list: true, }, 'archiveWorkingDirectory', 'hasOptionalFileInputs', ]; export default class ApplicationInterfaceDefinition extends BaseModel { constructor(data = {}) { super(FIELDS, data); } }