Create JavaScript Models from JSON

Author : Scott Lewis

Tags : javascript, algorithms, data structures

For the past several months I have been working on a new Adobe Illustrator extension to make the functionality of IconJar available as a panel. In order to manipulate the IconJar data - which is stored in JSON, I needed to create "Plain Old JavaScript Objects" (POJsO?) with getters and setters. Rather than type out the same lines of code over-and-over, I decided to create a command-line utility to quickly create the JavaScript classes for me.

Plain Old JavaScript Objects

Using an object with accessors (getters & setters) makes life easier. It does this by making the data objects more predictable. Accessing the properties will act consistently and reliably. Even referencing a property that hasn't been set won't throw an error. The DTO (Data Transfer Object) pattern can also provide formatting, validations, and type transformations such as rendering the data as JSON, an array, etc.

OpenSource NPM Package

I was pleased enough with the end result to release the code as an NPM package - @atomiclotus/json-to-js-model. The code is only alpha at the moment but I am making updates regularly. You can also download (or fork) the code from the json-to-js-model Github repository.

Rather than do a lot of search-and-replace and string concatenation, it occurred to me that generating the final output is no different from generating any other output so I use Handlebars.js to create the output from templates.

Formatting the JSON

An example of the JSON markup is below. The utility is smart enough to create an object with basic getters and setters from a JSON object. I also added a few simple features to enhance the output even more.

Notice the properties at the bottom of the code like __primaryKey. These are what I call "meta properties" that tell json-to-js-model a bit more about the object it is creating. There are only 5 meta properties current:

__primaryKey;

__parent;

__type;

__className;

The meaning of each should be easy to guess. __primaryKey is the name of the property that represents the object's unique identifier. In this example, "identifier" is the name of the primary key.

__parent refers to the parent collection, if there is one. This should point to the property that represents the primary key of the parent object/collection. In this example, the property that points to the parent's primary key just happens to be "parent".

__type refers to the type of json-to-js-model template to use. The current options are "item" and "collection". An item is a class representing a single data object. A collection is a group of items. In this example, the item is "Icon" and the collection is "IconSet". Json-to-js-model will create the necessary references for you.

__className refers to the name of the class to be created. Sticking with our example, the item class is Icon and the collection class is IconSet.

{
  "identifier::string"   : "51C4E9EC-9A0D-459B-B2B0-D06BAE877E77",
  "name::string"         : "girl-in-ballcap",
  "tags::string"         : "ballcap,girl,in",
  "file::string"         : "girl-in-ballcap.svg",
  "licence::string"      : "",
  "date::date"           : "2019-06-13 07:36:28",
  "width::number"        : 0,
  "height::number"       : 0,
  "parent::string"       : "F19A7973-0CB3-4751-B74F-E2AF0F9B2AF4",
  "type::string"         : 0,
  "unicode::string"      : "",

  "__primaryKey" : "identifier",
  "__parent"     : "parent",
  "__type"       : "item",
  "__className"  : "Icon"
}

The resulting code for the Icon class is shown below.

/*
 * Copyright (c) 2020.-present Atomic Lotus, LLC - Scott Lewis <scott@atomiclotus.net>
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

/* ============================================================ *
 * This file is auto-generated by JsonToJsModel by @atomiclotus *
 * ============================================================ */

((global, module, exports) => {
  class Icon {
    constructor(data) {
      if (!data) data = {};

      const uniqueId = this.generateUUID();

      this.instance = "Icon@" + uniqueId;

      /**
       * {string}
       */
      this.primaryKey = "identifier";

      /**
       * {string}
       */
      this.identifier = this._get(data, "identifier", uniqueId);

      /**
       * {string}
       */
      this.name = this._get(data, "name", null);

      /**
       * {string}
       */
      this.tags = this._get(data, "tags", null);

      /**
       * {string}
       */
      this.file = this._get(data, "file", null);

      /**
       * {string}
       */
      this.licence = this._get(data, "licence", null);

      /**
       * {date}
       */
      this.date = this._get(data, "date", new Date().toISOString());

      /**
       * {number}
       */
      this.width = this._get(data, "width", new Date().toISOString());

      /**
       * {number}
       */
      this.height = this._get(data, "height", new Date().toISOString());

      /**
       * {string}
       */
      this.parent = this._get(data, "parent", null);

      /**
       * {string}
       */
      this.type = this._get(data, "type", new Date().toISOString());

      /**
       * {string}
       */
      this.unicode = this._get(data, "unicode", null);
    }

    /**
     * Gets the value of identifier
     * @returns {string}
     */
    getIdentifier() {
      return this.identifier;
    }

    /**
     * Gets the value of name
     * @returns {string}
     */
    getName() {
      return this.name;
    }

    /**
     * Gets the value of tags
     * @returns {string}
     */
    getTags() {
      return this.tags;
    }

    /**
     * Gets the value of file
     * @returns {string}
     */
    getFile() {
      return this.file;
    }

    /**
     * Gets the value of licence
     * @returns {string}
     */
    getLicence() {
      return this.licence;
    }

    /**
     * Gets the value of date
     * @returns {date}
     */
    getDate() {
      return this.date;
    }

    /**
     * Gets the value of width
     * @returns {number}
     */
    getWidth() {
      return this.width;
    }

    /**
     * Gets the value of height
     * @returns {number}
     */
    getHeight() {
      return this.height;
    }

    /**
     * Gets the value of parent
     * @returns {string}
     */
    getParent() {
      return this.parent;
    }

    /**
     * Gets the value of type
     * @returns {string}
     */
    getType() {
      return this.type;
    }

    /**
     * Gets the value of unicode
     * @returns {string}
     */
    getUnicode() {
      return this.unicode;
    }

    /**
     * Sets the value of identifier
     * @param {string} value  The value to set identifier to.
     * @returns {string}
     */
    setIdentifier(value) {
      if (!value instanceof String) {
        throw new TypeError("String required.");
      }
      this.identifier = value;
      return this.identifier;
    }

    /**
     * Sets the value of name
     * @param {string} value  The value to set name to.
     * @returns {string}
     */
    setName(value) {
      if (!value instanceof String) {
        throw new TypeError("String required.");
      }
      this.name = value;
      return this.name;
    }

    /**
     * Sets the value of tags
     * @param {string} value  The value to set tags to.
     * @returns {string}
     */
    setTags(value) {
      if (!value instanceof String) {
        throw new TypeError("String required.");
      }
      this.tags = value;
      return this.tags;
    }

    /**
     * Sets the value of file
     * @param {string} value  The value to set file to.
     * @returns {string}
     */
    setFile(value) {
      if (!value instanceof String) {
        throw new TypeError("String required.");
      }
      this.file = value;
      return this.file;
    }

    /**
     * Sets the value of licence
     * @param {string} value  The value to set licence to.
     * @returns {string}
     */
    setLicence(value) {
      if (!value instanceof String) {
        throw new TypeError("String required.");
      }
      this.licence = value;
      return this.licence;
    }

    /**
     * Sets the value of date
     * @param {date} value  The value to set date to.
     * @returns {date}
     */
    setDate(value) {
      if (!value instanceof Date) {
        throw new TypeError("Date required.");
      }
      this.date = value;
      return this.date;
    }

    /**
     * Sets the value of width
     * @param {number} value  The value to set width to.
     * @returns {number}
     */
    setWidth(value) {
      if (!value instanceof Number) {
        throw new TypeError("Number required.");
      }
      this.width = value;
      return this.width;
    }

    /**
     * Sets the value of height
     * @param {number} value  The value to set height to.
     * @returns {number}
     */
    setHeight(value) {
      if (!value instanceof Number) {
        throw new TypeError("Number required.");
      }
      this.height = value;
      return this.height;
    }

    /**
     * Sets the value of parent
     * @param {string} value  The value to set parent to.
     * @returns {string}
     */
    setParent(value) {
      if (!value instanceof String) {
        throw new TypeError("String required.");
      }
      this.parent = value;
      return this.parent;
    }

    /**
     * Sets the value of type
     * @param {string} value  The value to set type to.
     * @returns {string}
     */
    setType(value) {
      if (!value instanceof String) {
        throw new TypeError("String required.");
      }
      this.type = value;
      return this.type;
    }

    /**
     * Sets the value of unicode
     * @param {string} value  The value to set unicode to.
     * @returns {string}
     */
    setUnicode(value) {
      if (!value instanceof String) {
        throw new TypeError("String required.");
      }
      this.unicode = value;
      return this.unicode;
    }

    /**
     * Gets the value of an object property by name.
     * @param {object}  subject     The object to search.
     * @param {string}  key         The name of the property to get.
     * @param {*}       fallback    The default value to return if key is not found.
     */
    _get(subject, key, fallback) {
      if (typeof subject[key] !== "undefined") {
        return subject[key];
      }
      return fallback;
    }

    /**
     * Creates a unique identifier in UUID format.
     */
    generateUUID() {
      let d = new Date().getTime();
      if (
        typeof performance !== "undefined" &&
        typeof performance.now === "function"
      ) {
        d += performance.now(); //use high-precision timer if available
      }
      return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
        let r = (d + Math.random() * 16) % 16 | 0;
        d = Math.floor(d / 16);
        return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
      });
    }

    /**
     * Get the {Icon} as on object of key => value pairs.
     * @returns {Icon[]}
     */
    valueOf() {
      return {
        identifier: this.getIdentifier(),
        name: this.getName(),
        tags: this.getTags(),
        file: this.getFile(),
        licence: this.getLicence(),
        date: this.getDate(),
        width: this.getWidth(),
        height: this.getHeight(),
        parent: this.getParent(),
        type: this.getType(),
        unicode: this.getUnicode(),
      };
    }

    /**
     * Get the {Icon} as a JSON object.
     * @returns {string}
     */
    toJSON() {
      return JSON.stringify(this.valueOf());
    }
  }

  /*
   * A ttach to the parent scope.
   */
  if (typeof module !== "undefined" && module.exports) {
    module.exports = Icon;
  } else if (typeof exports === "object") {
    exports.Icon = Icon;
  }
})(this, module, exports);

json-to-js-model outputs both es5 and es6 classes by default so you have what you need regardless which version of JavasScript you are using. Take a look at the handlebars template for the above JavaScript class.

/*
 * Copyright (c) 2020.-present Atomic Lotus, LLC - Scott Lewis <scott@atomiclotus.net>
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 */

/* ============================================================ *
 * This file is auto-generated by JsonToJsModel by @atomiclotus *
 * ============================================================ */

((global, module, exports) => {

    class {{ClassName}} {

        constructor(data) {

            if (! data) data = {};

            const uniqueId = this.generateUUID();

            this.instance = '{{ClassName}}@' + uniqueId;

            /**
             * {string}
             */
            this.primaryKey = '{{primaryKey}}';

        {{#each properties}}
            /**
             * {{#bracket}}{{type}}{{/bracket}}
             */
            this.{{name}} = this._get(data, '{{name}}', {{#if primary}}uniqueId{{else}}{{defaultValue}}{{/if}});

        {{/each}}
        }

        {{#each getters}}
        /**
         * Gets the value of {{name}}
         * @returns {{#bracket}}{{type}}{{/bracket}}
         */
        {{getter}}() {
            return this.{{name}};
        }

        {{/each}}

        {{#each setters}}
        /**
         * Sets the value of {{name}}
         * @param {{#bracket}}{{type}}{{/bracket}} value  The value to set {{name}} to.
         * @returns {{#bracket}}{{type}}{{/bracket}}
         */
        {{setter}}(value) {
            if (! value instanceof {{ucWords type}}) {
                throw new TypeError('{{ucWords type}} required.');
            }
            this.{{name}} = value;
            return this.{{name}};
        }

        {{/each}}
        /**
         * Gets the value of an object property by name.
         * @param {object}  subject     The object to search.
         * @param {string}  key         The name of the property to get.
         * @param {*}       fallback    The default value to return if key is not found.
         */
        _get(subject, key, fallback) {
            if (typeof subject[key] !== 'undefined') {
                return subject[key];
            }
            return fallback;
        }

        /**
         * Creates a unique identifier in UUID format.
         */
        generateUUID() {
            let d = new Date().getTime();
            if (typeof performance !== 'undefined' && typeof performance.now === 'function'){
                d += performance.now(); //use high-precision timer if available
            }
            return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
                let r = (d + Math.random() * 16) % 16 | 0;
                d = Math.floor(d / 16);
                return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
            });
        }

        /**
         * Get the {{#bracket}}{{ClassName}}{{/bracket}} as on object of key => value pairs.
         * @returns {{#bracket}}{{ClassName}}[]{{/bracket}}
         */
        valueOf() {
            return {
            {{#each getters}}
                {{name}} : this.{{getter}}(),
            {{/each}}
            }
        }

        /**
         * Get the {{#bracket}}{{ClassName}}{{/bracket}} as a JSON object.
         * @returns {string}
         */
        toJSON() {
            return JSON.stringify(this.valueOf());
        }
    }

    /*
     * A ttach to the parent scope.
     */
    if (typeof module !== 'undefined' && module.exports) {
        module.exports = {{ClassName}};
    }
    else if ( typeof exports === 'object' ){
        exports.{{ClassName}} = {{ClassName}};
    }

})(this, module, exports);

At the moment I have only very basic unit testing in place. My focus has been on getting the code working for my needs but it appears to work quite well. It isn't rocket science. The technique is actually pretty simple but it has saved me a lot of time coding repetitive getters and setters.

Let me know if you try out the utility and what the result is. You can open an issue on Github and I will address them as time permits.

Posted in Adobe Illustrator | Tag: javascript, algorithms, data structures

Pay it forward

If you find value in the work on this blog, please consider paying it forward and donating to one of the followign charities that are close to my heart.