{"id":847,"date":"2018-01-20T08:06:32","date_gmt":"2018-01-20T14:06:32","guid":{"rendered":"https:\/\/www.joshualyman.com\/?p=847"},"modified":"2018-01-20T08:06:32","modified_gmt":"2018-01-20T14:06:32","slug":"simple-javascript-model-mapper-utility-functions","status":"publish","type":"post","link":"https:\/\/www.joshualyman.com\/2018\/01\/simple-javascript-model-mapper-utility-functions\/","title":{"rendered":"Simple Javascript Model Mapper Utility Functions"},"content":{"rendered":"

I bundled together some very useful code that I’ve used on several projects and put it up on GitHub for use by others:<\/p>\n

https:\/\/github.com\/jlyman\/simple-model-mapper<\/a><\/strong><\/p>\n

Here’s a copy of the README file. Check out the repo for the actual code.<\/p>\n


\n

This small collection of methods make it easier to map between the data structure returned from or sent to and API, and a correspondingly equivalent Javascript object model.<\/p>\n

Given an API response object you can turn it into a Javascript model by calling mapApiToModel(apiResponse, modelMap, ModelType)<\/code>, or go the other direction by calling mapModelToApi(model, modelMap)<\/code>.<\/p>\n

Motivation<\/h2>\n

I often find myself in situations where a project has a model both on the server and on the client, transferred via an API, but with ever so slight differences between the two that preclude just using a straight reading of the JSON or JSON.stringify()<\/code>. Sometimes the API snake_cases<\/code> the properties, or sometimes a property is collapsed into one coming from the API, but needs to be split into different properties on the client.<\/p>\n

Because this concern occurs again and again, it makes sense to centralize the translation of API response object <==> Javascript object model, standardize it, and pull it out so the right part of your application can have concern over it.<\/p>\n

Example<\/h2>\n

Say we have a very simple User model on both the client and server, which looks like the following in JS:<\/p>\n

User {\n  id: 83924,\n  username: 'george',\n  isAdmin: false,\n  permissions: ['read', 'update'],\n}\n<\/code><\/pre>\n

But coming from the API, the response JSON looks like this:<\/p>\n

{\n  \"id\": 83924,\n  \"username\": \"george\",\n  \"user_perms\": [\n    \"read\",\n    \"update\",\n  ],\n}\n<\/code><\/pre>\n

Note the casing is different, isAdmin<\/code> is missing, and permissions<\/code> is represented as user_perms<\/code>. You can’t just all JSON.parse(apiResponse)<\/code> on this to get your User model in JS.<\/p>\n

Instead, we can build a quick bi-directional mapping of properties between the two, and then call the appropriate mapper function in these utility functions to translate to the desired object.<\/p>\n

Properties Map<\/h3>\n

For our User model, the mapping is an array of arrays, each child array representing a mapping of a property. The array can either be two strings, providing a one-to-one map between JS property name and API name, or a string and two functions, providing a JS property name and a function to map the API object to the model, and the third vice versa.<\/p>\n

Here’s the map for our User model:<\/p>\n

const userToApiMap = [\n  ['id', 'id'],\n  ['username', 'userName'],\n  ['isAdmin',\n    apiObj => {\n      const isAdmin = apiObj.user_perms.indexOf('admin') !== -1;\n      const permissions = apiObj.user_perms.filter(p => p !== 'admin');\n      return {\n        isAdmin,\n        permissions,\n      };\n    },\n    modelObj => {\n      return {\n        user_perms: modelObj.permissions.concat(modelObj.isAdmin && ['admin']),\n      };\n    }\n  ]\n]\n<\/code><\/pre>\n

Note that id<\/code> and username<\/code> are super straightforward; they’re just a one-to-one mapping of a simple property, with username<\/code> just having a little different casing.<\/p>\n

But isAdmin<\/code> and permissions<\/code> on the client are more involved. To determine if a user is an admin (boolean), we have to see if there is an admin<\/code> string in the user_perms<\/code> array. And we want our permissions<\/code> array (client-side) to have all permissions except<\/em> for 'admin'<\/code>, if present. So we write two small functions to translate between the two.<\/p>\n

The return value of these functions gets concatenated in with the rest of the object, so we can return more than one property and they will all be placed on the target object. (This also means that, even though they are inside of an array targeting isAdmin<\/code>, nothing will by default be assigned to isAdmin<\/code> if you don’t return it.)<\/p>\n

Performing the mapping<\/h3>\n

Now that we have our mapping between the two defined, we can quickly perform the translation by using two of the utility methods defined: mapModelToApi()<\/code> and mapApiToModel()<\/code>.<\/p>\n

mapModelToApi<\/code><\/h4>\n

mapModelToApi(model, modelMap)<\/code><\/p>\n