Writing boring code is not fun. Especially if that code is just a derivative of some data. You should generate it instead! A great example is to generate your HTTP client code from an API definition. This is actually very easy to do with a recent node version and requires almost no extra dependencies. In this post I will show you how to take a JSON schemadefinition and generate some validation logic from that.
Code generation
With code generation, you start with a piece of data. You use that data to generate some code that you can use in your project. Here are some real world examples:
- Generate HTTP client code from an API definition.
- Generate flowtype definitions for objects from a JSON Schema.
- Generate a list of module exports from a long list of files.
Our example
In this example, we start with this JSON Schema definition:
{ "title": "Example Schema", "type": "object", "properties": { "firstName": { "type": "string" }, "lastName": { "type": "string" }, "age": { "description": "Age in years", "type": "number", "minimum": 0 } }, "required": ["firstName", "lastName"] }
We use that data to generate this function from it:
function validate(obj) {
// Check required properties if (!obj.hasOwnProperty('firstName')) { return false } if (!obj.hasOwnProperty('lastName')) { return false }
return true }
This very limited example serves only to explain the process you can use to generate code with node. There are existing tools that let you do runtime validation of objects based on a JSON Schema.
Template literals in JavaScript
We are going to use template literals. Here’s a quick reminder what they are. If you use backticks to surround a string: `hello` instead of "hello", you’re using template literals. There are two features that make them really great for templating source code.
- They can include newlines, so you don’t have to "add\n" + "all\n" + "lines\n"
- You can use string substitution to put variables in your string`Hello ${name}`
If your node version is greater than 4.3.2 it is safe to use template literals. Node currently supports most modern JavaScript language features. Use node.green to check for support.
Checking for required attributes
The simplest code to generate from this schema is a check for required properties:
function checkRequired (propName) { return `if(!obj.hasOwnProperty('${propName}')) { return false }` }
console.log(checkRequired('firstName'));
We use hasOwnProperty to check if the property is set. The output from this snippet is:
if(!obj.hasOwnProperty('firstname')) { return false }
Let’s use this function to generate a real module from our schema:
// schema.json has this property: //"required": ["firstName", "lastName"]
const schema = require('./schema.json')
function generateCode (schema) { return `module.exports = function validate(obj){ ${schema.required.map(checkRequired).join('\n')} return true }` }
console.log(generateCode(schema))
Formatting the output with esformatter
When we run this, the output is barely readable. All the indentation is messed up. To solve this we use esformatter. This has the added benefit that we can only output valid JavaScript:
console.log(esformatter.format(generateCode(schema)))
module.exports = function validate(obj) { if (!obj.hasOwnProperty('firstName')) { return false } if (!obj.hasOwnProperty('lastName')) { return false } return true; }
As you can see, we now generated a real module. If you want, you can configure esformatter to change the output to your liking. There also is a list of plugins that can make the output even better.
Handling parse errors in the output
You don’t get nice error messages with esformatter. If you let it format invalid JavaScript, you just get SyntaxError: Unexpected token. There are two ways to fix this.
- You can output the entire string without esformatter and try to find your error.
- What I always do is just prepend my last change with a //. This is a bit awkward but it is manageable. Like when I misspelled return I let it generate this:
if (!obj.hasOwnProperty('firstName')) { // retun false }
Saving the file
The one thing left is actually saving the file and adding it to the build process.
const fs = require('fs') const schema = require('./schema.json') const source = esformatter.format(generateCode(schema)) fs.writeFileSync('./generated/validate.js', source, 'utf-8')
You can add a postinstall script to your package.json. This will always generate the code after you run npm install. You can also choose to generate it just before run. Pick whatever suits your use case.
I would recommend to keep the data in version control and add the generated code to your ignore file. In this example, only schema.json is added to version control.
"scripts": { "postinstall": "node generate", "start": "node index", "test": "node test" }
Example Repository
I made an example repository you can use for inspiration. It has some more features, like checking the types of the properties and validating the range of ‘age’.
Check it out at https://gitlab.com/wietsevenema/js-code-generation/tree/master
Please share your use cases for code generation in the responses. There are a lot of situations where you write code that is just a derivative of some piece of data.
Remember: Friends don’t let friends write code that could have been generated