We’re big fans of Backbone here at Atlassian. It’s already used in the majority of our products, and its uptake within the company is increasing.

Backbone is unopinionated by design. It tries hard not to get in the way of any competing libraries and frameworks you might be using, or any coding conventions you practice. This means that, out of the box, Backbone has some shortcomings, waiting for you to custom-tailor to your own purposes. And the open-source community doesn’t disappoint. There is a full ecosystem of add-ons to choose from when you want to fill the gaps.

One case where Backbone is wide-open for enhancement? Non-primitive attributes. Out-of-the-box it only supports primitives well. Because Backbone doesn’t support nested cloning, objects including plain JSON objects, won’t convert well when you call toJSON() on your model. For example:

1
2
3
4
5
6
7
8
var model = new Backbone.Model({
    things : [],
    prop : 'a'
});
var json = model.toJSON(); // { things : [], prop : 'a' }
json.prop = 'b';
json.things[0] = 'c';
model.toJSON(); // { things : [ 'c' ], prop : 'a' } 'c' was added!!

Primitive properties are cloned, so the JSON is safe to distribute without the model itself being affected.
But the array attribute ‘things’ is not. The array provided by toJSON is the exact same array that is still on the model, and as you can see, changing it will affect the model in spooky ways.

The other direction – converting JSON into nested Models – is hard too. In Stash we have REST endpoints that return complex objects. A PullRequest includes information about its source and target branches, which include information about their parent Repository, and a Repository includes information its parent Project. It’d be great for us if they were automatically converted into the correct Backbone models based solely on the provided JSON.

Here’s the bare minimum we needed to support in Stash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var json = {
    id : 1,
    // ... other fields ...
    fromRef : {
        id : 'deadbeef',
        repository : {
            slug : 'REPO',
            name : 'repo',
            project : {
                key : 'PROJ',
                name : 'Project'
            }
        }
    }
}
// I can create complex Backbone models from a blob of JSON
var pullRequest = new PullRequest(json);
// I can access nested models
var sourceProject = pullRequest.getFromRef().getRepository().getProject();
// The JSON structure is maintained, with a deep-copy
deepEquals(pullRequest.toJSON(), json);

So a few months ago, I added some code to Brace (which Johnathon Creenaune blogged earlier) to take care of this. Let’s compare.

If you want to support the above usage, with validation and coercion, using raw Backbone (no Brace), here’s how you would need to define a PullRequest.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// Check that primitives are the correct type
function checkTypeof(obj, prop, type) {
    if (obj[prop] != null && typeof obj[prop] !== type) {
        return prop + ' must be a ' + type;
    }
}

// Check that no additional properties are specified
// accidentally (this could signify a typo'd property or
// out-of-date Backbone model)
function checkUnknownProps(obj, knownProps) {
    var unknownProps = _.filter(_.keys(obj), function(key) {
        return !_.contains(knownProps, key);
    });
    if (unknownProps.length) {
        return 'Invalid properties: ' + unknownProps.join(', ');
    }
}

// Save a reference to Backbone's default toJSON function
var toJSON = Backbone.Model.prototype.toJSON;

// a toJSON function that will call toJSON on nested models.
// Note: in the interest of simplicity, this still doesn't handle
// cloning of arrays.
function getNestedModelsToJSONFn(modelProps) {
    return function() {
        return _.reduce(modelProps, function(json, modelProp) {
            json[modelProp] = json[modelProp].toJSON();
            return json;
        }, toJSON.call(this));
    };
}

var Project = Backbone.Model.extend({
    validate : function(attrs) {
        return checkTypeof(attrs, 'key', 'string') ||
               checkTypeof(attrs, 'name', 'string') ||
               checkUnknownProps(attrs, ['key', 'name']);
    }
});

var Repository = Backbone.Model.extend({
    initialize : function() {
        // coerce a plain JSON object into a Project model on initialize.
        if ($.isPlainObject(this.get('project'))) {
            this.set('project', new Project(this.get('project')));
        }
    },
    validate : function(attrs) {
        if (attrs.project && !(attrs.project instanceof Project)) {
            return "Repository's project property must be a Project";
        }
        return checkTypeof(attrs, 'name', 'string') ||
               checkTypeof(attrs, 'slug', 'string') ||
               checkUnknownProps(attrs, ['name', 'slug', 'project']);
    },
    getProject : function() { return this.get('project'); },
    toJSON : getNestedModelsToJSONFn(['project'])
});

var Ref = Backbone.Model.extend({
    initialize : function() {
        if ($.isPlainObject(this.get('repository'))) {
            this.set('repository', new Repository(this.get('repository')));
        }
    },
    validate : function(attrs) {
        if (attrs.repository && !(attrs.repository instanceof Repository)) {
            return "Ref's repository property must be a Repository";
        }
        return checkTypeof(attrs, 'id', 'string') ||
               checkUnknownProps(attrs, ['id', 'repository']);
    },
    getRepository: function() { return this.get('repository'); },
    toJSON : getNestedModelsToJSONFn(['repository'])
});

var PullRequest = Backbone.Model.extend({
    initialize : function() {
        if ($.isPlainObject(this.get('fromRef'))) {
            this.set('fromRef', new Ref(this.get('fromRef')));
        }
    },
    validate : function(attrs) {
        if (attrs.fromRef && !(attrs.fromRef instanceof Ref)) {
            return "PullRequest's fromRef property must be a Ref";
        }
        return checkTypeof(attrs, 'id', 'number') ||
               // ...
               checkUnknownProps(attrs, ['id', /*...,*/ 'fromRef']);
    },
    getFromRef: function() { return this.get('fromRef'); },
    toJSON : getNestedModelsToJSONFn(['fromRef'])
});

In my mind, that’s a lot of bug-prone, hard-to-read boilerplate for creating a model.

With Brace, the code is much simpler.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var Project = Brace.Model.extend({
    namedAttributes : {
        'key' : 'string',
        'name' : 'string'
    }
});

var Repository = Brace.Model.extend({
    namedAttributes : {
        'name' : 'string',
        'slug' : 'string',
        'project' : Project
    }
});

var Ref = Brace.Model.extend({
    namedAttributes : {
        id : 'string',
        repository : Repository
    }
});

var PullRequest = Brace.Model.extend({
    namedAttributes : {
        id : 'number',
        //...
        fromRef : Ref
    }
});

Now it’s one-third of the lines, and so much easier to read. Brace lets you declaratively define the types of your Model’s properties using the namedAttributes property.

When you pass in a string, it will use typeof to validate the type of that property. When you give it a function, it will use that function as a constructor and coerce any input to be instanceof that function. And with syntax inspired by Mongoose, passing in an array like [ ‘number’ ] as the type signifies that the property must be an array of numbers.

Brace is open-source and available on Bitbucket. The readme contains more details on the syntax. Check it out!

If validation and coercion aren’t your cup of tea, or you’re interested in separating your related models in your JSON, also check out Backbone-relational which offers alternative ways to represent your object relationships, but without the type validation.