views:

183

answers:

5

What's the quickest and easiest way to convert my json, containing the data of the objects, into actual objects with methods attached?

By way of example, I get data for a fruitbowl with an array of fruit objects which in turn contain an array of seeds thus:

{"fruitbowl": [{
     "name": "apple", 
     "color": "red", 
     "seeds": []
   },{
     "name": "orange", 
     "color": "orange", 
     "seeds": [
        {"size":"small","density":"hard"},
        {"size":"small","density":"soft"}
    ]}
}

That's all nice and good but down on the client we do stuff with this fruit, like eat it and plant trees...

 var fruitbowl = []
 function Fruit(name, color, seeds){
     this.name = name
     this.color = color
     this.seeds = seeds
     this.eat = function(){
         // munch munch
     }
 }
 function Seed(size, density){
     this.size = size
     this.density = density
     this.plant = function(){
          // grow grow
     }
 }

My ajax's success routine currently is currently looping over the thing and constructing each object in turn and it doesn't handle the seeds yet, because before I go looping over seed constructors I'm thinking

Is there not a better way?

    success: function(data){           
        fruitbowl.length = 0
        $.each(data.fruitbowl, function(i, f){
            fruitbowl.push(new Fruit(f.name, f.color, f.seeds))
        })

I haven't explored looping over the objects as they are and attaching all the methods. Would that work?

+2  A: 

Yes, it would work, but it's not desirable. Apart from appearing slightly hacky IMO, you're attaching methods to each instance of your fruit and seeds, where you should instead be using the prototype chain. If you're going to be using instanceof in the future, this method won't work anyway.

What you're currently doing is the best solution; and you'll be able to use instanceof.

If you're feeling adventurous, you can use JSONP instead of AJAX, with the JSONP response looking something like:

buildFruitbowl([new Fruit("orange", "blue", [new Seed("small", "hard"), new Seed("big", "soft")]), new Fruit("banana", "yellow", [new Seed("small", "hard"), new Seed("big", "soft")])]);

Which will save you having to do all your object looping, and you'll get your Fruit and Seeds how you want (and instanceof support); however I would still stick to what you're doing already.

Best of look growing your bananas.

Matt
"Best of look growing your bananas" - never heard that one before.
Anurag
A: 

Using D Crockford's "json2" library, you can supply a "reviver" function to the parsing process. The reviver function is passed each key and each value, and should return the actual effective value to be used in the parsed result.

There's a corresponding optional parameter in the "stringify" method.

Pointy
warning: whenever you decide to utilize the reviver you need to force an overwrite of mozilla/firefox < 3.6 native JSON with json2.js. There is a documented bug in the reviver implementation.
Sky Sanders
A: 

Pass the data to the object constructor then use jquery's "extend" to combine the data and methods:

 function Fruit(data){
     $.extend(this, data)
     this.eat = function(){
         // munch munch
     }
 }
 ...
       $.each(data.fruitbowl, function(i, f){
           fruitbowl.push(new Fruit(f))
       })

You still have loops involved; and must manually code loops for the nested objects (like seeds), but still a very simple way to get past the problem.

John Mee
A: 

You could modify the JSON structure to store the type information. If you have a lot of objects to serialize and deserialize back and forth, this would save time writing custom code for each object.

Also note, this modifies the JSON structure and adds a __type__ property to each custom object. I think this is a cleaner approach than keeping separate configuration files. So without further ado, this is how it basically works:

var fruitBowl = {..};
fruitBowl[0].eat();
fruitBowl[1].seeds[0].plant();

call serialize on the object to get a JSON representation

var json = fruitBowl.serialize();

call deserialize on the JSON encoded string to reconstruct the objects

var resurrected = json.deserialize();

now you can access properties and call methods on the objects:

resurrected[0].eat();
resurrected[1].seeds[0].plant();

It works for any levels of deeply nested objects, although it might be a little buggy for now. Also it is most likely not cross-browser (only tested on Chrome). Since the deserializer is not familiar with an object's constructor function, it basically creates each custom object without passing any parameters. I've setup a working demo on jsfiddle at http://jsfiddle.net/kSATj/1/.

The constructor function had to be modified to account for the two ways it's objects could be created

  1. Directly in Javascript
  2. Reconstructed from JSON

All constructors would need to accommodate creation from both ends, so each property needs to be assigned a default fallback value incase nothing was passed.

function SomeObject(a, b) {
    this.a = a || false; // defaultValue can be anything
    this.b = b || null; // defaultValue can be anything
}

// one type of initialization that you can use in your code
var o = new SomeObject("hello", "world");

// another type of initialization used by the deserializer
var o = new SomeObject();;
o.a = "hello";
o.b = "world";

For reference, the modified JSON looks like:

{"fruitbowl": 
    [
        {
            "__type__": "Fruit",
            "name": "apple",
            "color": "red",
            "seeds": []           
        },
        {
            "__type__": "Fruit",
            "name": "orange",
            "color": "orange",
            "seeds": 
            [
                {
                    "__type__": "Seed",
                    "size": "small",
                    "density": "hard"
                },
                {
                    "__type__": "Seed",
                    "size": "small",
                    "density": "soft"
                }
            ]
        }
    ]
}

This is just a helper function to identify simple types:

function isNative(object) {
    if(object == null) {
        return true;
    }

    var natives = [Boolean, Date, Number, String, Object, Function];
    return natives.indexOf(object.constructor) !== -1;
}

Serializes an object into JSON (with type info preserved):

Object.prototype.serialize = function() {
    var injectTypes = function(object) {
        if(!isNative(object)) {
            object.__type__ = object.constructor.name;
        }

        for(key in object) {
            var property = object[key];
            if(object.hasOwnProperty(key) && !isNative(property)) {
                injectTypes(property);
            }
        }
    };

    var removeTypes = function(object) {
        if(object.__type) {
            delete object.__type__;
        }
        for(key in object) {
            var property = object[key];
            if(object.hasOwnProperty(key) && !isNative(property)) {
                removeTypes(property);
            }
        }
    }

    injectTypes(this);
    var json = JSON.stringify(this);
    removeTypes(this);

    return json;
};

Deserialize (with custom objects reconstructed):

String.prototype.deserialize = function() {
    var rawObject = JSON.parse(this.toString());

    var reconstruct = function(object) {
        var reconstructed = {};

        if(object.__type__) {
            reconstructed = new window[object.__type__]();
            delete object.__type__;
        }
        else if(isNative(object)) {
            return object;
        }

        for(key in object) {
            var property = object[key];

            if(object.hasOwnProperty(key)) {
                reconstructed[key] = reconstruct(property);
            }
        }

        return reconstructed;
    }

    return reconstruct(rawObject);
};
Anurag
Wow, full marks for effort! Not many for being easier. The idea of passing __type__ is noted.
John Mee
I agree, this is not very helpful if you have few objects. Building objects by hand from JSON is definitely a better and easier approach in that case. I think I've already mentioned the drawbacks with my solution - *buggy*, *not cross-browser*, and *pollutes JSON*, but with a little more effort this approach should start paying off as the number of objects that need to be reconstructed goes up.
Anurag
A: 

John,

Hopefully not too late to chip in here. I had a very similar problem just last week and solved it with the following piece of js (it could easily be converted to jquery as well.).

Here's the base usage:

    $(document).ready(function() {
        var bowl = { "fruitbowl": [{
            "name": "apple",
            "color": "red",
            "seeds": []
        },
        {
            "name": "orange",
            "color": "orange",
            "seeds": [
            { "size": "small", "density": "hard" },
            { "size": "small", "density": "soft"}]
        }
        ]
        };

        var serialized = jsonToObject.serialize(bowl);
        var deserialized = jsonToObject.deserialize(serialized);
        // basic tests on serialize/deserializing...
        alert(deserialized.fruitbowl[0].name);
        alert(deserialized.fruitbowl[1].seeds[0].density);
    });

and here's the jsonToObject.js file:

jsonToObject = {

    deserialize: function(_obj) {
        if (typeof (JSON) === 'object' && typeof (JSON.parse) === 'function') {
            // native JSON parsing is available.
            //return JSON.parse(_obj);
        }
        // otherwise, try non-native methods
        var jsonValue = new Function("return " + _obj)();

        if (!jsonValue instanceof Object) {
            jsonValue = eval("(" + _obj + ")");
        }
        return jsonValue;
    },

    serialize: function(_obj) {
        // Let Gecko browsers do this the easy way - not working
        if (_obj != undefined && typeof _obj.toSource !== 'undefined'
            && typeof _obj.callee === 'undefined') {
            return _obj.toSource();
        }

        // Other browsers must do it the hard way
        switch (typeof _obj) {
            // numbers, booleans, and functions are trivial:                 
            // just return the object itself since its default .toString()                 
            // gives us exactly what we want                 
            case 'number':
            case 'boolean':
            case 'function':
                return _obj;
                break;

            // for JSON format, strings need to be wrapped in quotes                 
            case 'string':
                return '"' + _obj.replace(/"/mg, "'") + '"';
                break;

            case 'object':
                var str;
                if (_obj.constructor === Array || typeof _obj.callee !== 'undefined') {
                    str = '[';
                    var i, len = _obj.length;
                    for (i = 0; i < len - 1; i++) { str += this.serialize(_obj[i]) + ','; }
                    str += this.serialize(_obj[i]) + ']';
                }
                else {
                    str = '{';
                    var key;
                    for (key in _obj) { str += key + ':' + this.serialize(_obj[key]) + ','; }
                    str = str.replace(/\,$/, '') + '}';
                }
                return str;
                break;

            default:
                return '""';
                break;
        }
    }
}

hope this helps...

jim

[edit] - you could of course also give the two functions their prototype signatures in keeping with the excellent example above, ie..

String.prototype.deserialize = function() {...} Object.prototype.serialize = function() {...}

jim
thanks for the idea. deserialize is always done on strings so it should ideally be added to the String prototype.
Anurag
yes, i did do a local version that used the prototype sigs, but left my example as is given that yours takes this approach - gives a little variety :)btw - you still need to update the signiture to string in your example (http://jsfiddle.net/kSATj/) - just in case you overlook it ;)
jim
Variety is always good :) and thanks for the string tip. Just updated my example
Anurag