Turning JSON strings into objects with methods

12,736

Solution 1

To do this, you'll want to use a "reviver" function when parsing the JSON string (and a "replacer" function or a toJSON function on your constructor's prototype when creating it). See Section 15.12.2 and 15.12.3 of the specification. If your environment doesn't yet support native JSON parsing, you can use one of Crockford's parsers (Crockford being the inventor of JSON), which also support "reviver" functions.

Here's a simple bespoke example that works with ES5-compliant browsers (or libraries that emulate ES5 behavior) (live copy, run in Chrome or Firefox or similar), but look after the example for a more generalized solution.

// Our constructor function
function Foo(val) {
  this.value = val;
}
Foo.prototype.nifty = "I'm the nifty inherited property.";
Foo.prototype.toJSON = function() {
  return "/Foo(" + this.value + ")/";
};

// An object with a property, `foo`, referencing an instance
// created by that constructor function, and another `bar`
// which is just a string
var obj = {
  foo: new Foo(42),
  bar: "I'm bar"
};

// Use it
display("obj.foo.value = " + obj.foo.value);
display("obj.foo.nifty = " + obj.foo.nifty);
display("obj.bar = " + obj.bar);

// Stringify it with a replacer:
var str = JSON.stringify(obj);

// Show that
display("The string: " + str);

// Re-create it with use of a "reviver" function
var obj2 = JSON.parse(str, function(key, value) {
  if (typeof value === "string" &&
      value.substring(0, 5) === "/Foo(" &&
      value.substr(-2) == ")/"
     ) {
    return new Foo(value.substring(5, value.length - 2));
  }
  return value;
});

// Use the result
display("obj2.foo.value = " + obj2.foo.value);
display("obj2.foo.nifty = " + obj2.foo.nifty);
display("obj2.bar = " + obj2.bar);

Note the toJSON on Foo.prototype, and the function we pass into JSON.parse.

The problem there, though, is that the reviver is tightly coupled to the Foo constructor. Instead, you can adopt a generic framework in your code, where any constructor function can support a fromJSON (or similar) function, and you can use just one generalized reviver.

Here's an example of a generalized reviver that looks for a ctor property and a data property, and calls ctor.fromJSON if found, passing in the full value it received (live example):

// A generic "smart reviver" function.
// Looks for object values with a `ctor` property and
// a `data` property. If it finds them, and finds a matching
// constructor that has a `fromJSON` property on it, it hands
// off to that `fromJSON` fuunction, passing in the value.
function Reviver(key, value) {
  var ctor;

  if (typeof value === "object" &&
      typeof value.ctor === "string" &&
      typeof value.data !== "undefined") {
    ctor = Reviver.constructors[value.ctor] || window[value.ctor];
    if (typeof ctor === "function" &&
        typeof ctor.fromJSON === "function") {
      return ctor.fromJSON(value);
    }
  }
  return value;
}
Reviver.constructors = {}; // A list of constructors the smart reviver should know about  

To avoid having to repeat common logic in toJSON and fromJSON functions, you could have generic versions:

// A generic "toJSON" function that creates the data expected
// by Reviver.
// `ctorName`  The name of the constructor to use to revive it
// `obj`       The object being serialized
// `keys`      (Optional) Array of the properties to serialize,
//             if not given then all of the objects "own" properties
//             that don't have function values will be serialized.
//             (Note: If you list a property in `keys`, it will be serialized
//             regardless of whether it's an "own" property.)
// Returns:    The structure (which will then be turned into a string
//             as part of the JSON.stringify algorithm)
function Generic_toJSON(ctorName, obj, keys) {
  var data, index, key;

  if (!keys) {
    keys = Object.keys(obj); // Only "own" properties are included
  }

  data = {};
  for (index = 0; index < keys.length; ++index) {
    key = keys[index];
    data[key] = obj[key];
  }
  return {ctor: ctorName, data: data};
}

// A generic "fromJSON" function for use with Reviver: Just calls the
// constructor function with no arguments, then applies all of the
// key/value pairs from the raw data to the instance. Only useful for
// constructors that can be reasonably called without arguments!
// `ctor`      The constructor to call
// `data`      The data to apply
// Returns:    The object
function Generic_fromJSON(ctor, data) {
  var obj, name;

  obj = new ctor();
  for (name in data) {
    obj[name] = data[name];
  }
  return obj;
}

The advantage here being that you defer to the implementation of a specific "type" (for lack of a better term) for how it serializes and deserializes. So you might have a "type" that just uses the generics:

// `Foo` is a constructor function that integrates with Reviver
// but doesn't need anything but the generic handling.
function Foo() {
}
Foo.prototype.nifty = "I'm the nifty inherited property.";
Foo.prototype.spiffy = "I'm the spiffy inherited property.";
Foo.prototype.toJSON = function() {
  return Generic_toJSON("Foo", this);
};
Foo.fromJSON = function(value) {
  return Generic_fromJSON(Foo, value.data);
};
Reviver.constructors.Foo = Foo;

...or one that, for whatever reason, has to do something more custom:

// `Bar` is a constructor function that integrates with Reviver
// but has its own custom JSON handling for whatever reason.
function Bar(value, count) {
  this.value = value;
  this.count = count;
}
Bar.prototype.nifty = "I'm the nifty inherited property.";
Bar.prototype.spiffy = "I'm the spiffy inherited property.";
Bar.prototype.toJSON = function() {
  // Bar's custom handling *only* serializes the `value` property
  // and the `spiffy` or `nifty` props if necessary.
  var rv = {
    ctor: "Bar",
    data: {
      value: this.value,
      count: this.count
    }
  };
  if (this.hasOwnProperty("nifty")) {
    rv.data.nifty = this.nifty;
  }
  if (this.hasOwnProperty("spiffy")) {
    rv.data.spiffy = this.spiffy;
  }
  return rv;
};
Bar.fromJSON = function(value) {
  // Again custom handling, for whatever reason Bar doesn't
  // want to serialize/deserialize properties it doesn't know
  // about.
  var d = value.data;
      b = new Bar(d.value, d.count);
  if (d.spiffy) {
    b.spiffy = d.spiffy;
  }
  if (d.nifty) {
    b.nifty = d.nifty;
  }
  return b;
};
Reviver.constructors.Bar = Bar;

And here's how we might test that Foo and Bar work as expected (live copy):

// An object with `foo` and `bar` properties:
var before = {
  foo: new Foo(),
  bar: new Bar("testing", 42)
};
before.foo.custom = "I'm a custom property";
before.foo.nifty = "Updated nifty";
before.bar.custom = "I'm a custom property"; // Won't get serialized!
before.bar.spiffy = "Updated spiffy";

// Use it
display("before.foo.nifty = " + before.foo.nifty);
display("before.foo.spiffy = " + before.foo.spiffy);
display("before.foo.custom = " + before.foo.custom + " (" + typeof before.foo.custom + ")");
display("before.bar.value = " + before.bar.value + " (" + typeof before.bar.value + ")");
display("before.bar.count = " + before.bar.count + " (" + typeof before.bar.count + ")");
display("before.bar.nifty = " + before.bar.nifty);
display("before.bar.spiffy = " + before.bar.spiffy);
display("before.bar.custom = " + before.bar.custom + " (" + typeof before.bar.custom + ")");

// Stringify it with a replacer:
var str = JSON.stringify(before);

// Show that
display("The string: " + str);

// Re-create it with use of a "reviver" function
var after = JSON.parse(str, Reviver);

// Use the result
display("after.foo.nifty = " + after.foo.nifty);
display("after.foo.spiffy = " + after.foo.spiffy);
display("after.foo.custom = " + after.foo.custom + " (" + typeof after.foo.custom + ")");
display("after.bar.value = " + after.bar.value + " (" + typeof after.bar.value + ")");
display("after.bar.count = " + after.bar.count + " (" + typeof after.bar.count + ")");
display("after.bar.nifty = " + after.bar.nifty);
display("after.bar.spiffy = " + after.bar.spiffy);
display("after.bar.custom = " + after.bar.custom + " (" + typeof after.bar.custom + ")");

display("(Note that after.bar.custom is undefined because <code>Bar</code> specifically leaves it out.)");

Solution 2

You can indeed create an empty instance and then merge the instance with the data. I recommend using a library function for ease of use (like jQuery.extend).

You had some errors though (function ... = function(...), and JSON requires keys to be surrounded by ").

http://jsfiddle.net/sc8NU/1/

var data = '{"label": "new object"}';  // JSON
var inst = new Obj;                    // empty instance
jQuery.extend(inst, JSON.parse(data)); // merge

Note that merging like this sets properties directly, so if setLabel is doing some checking stuff, this won't be done this way.

Solution 3

So far as I know, this means moving away from JSON; you're now customizing it, and so you take on all of the potential headaches that entails. The idea of JSON is to include data only, not code, to avoid all of the security problems that you get when you allow code to be included. Allowing code means that you have to use eval to run that code and eval is evil.

Solution 4

If you want to use the setters of Obj :

Obj.createFromJSON = function(json){
   if(typeof json === "string") // if json is a string
      json = JSON.parse(json); // we convert it to an object
   var obj = new Obj(), setter; // we declare the object we will return
   for(var key in json){ // for all properties
      setter = "set"+key[0].toUpperCase()+key.substr(1); // we get the name of the setter for that property (e.g. : key=property => setter=setProperty
      // following the OP's comment, we check if the setter exists :
      if(setter in obj){
         obj[setter](json[key]); // we call the setter
      }
      else{ // if not, we set it directly
         obj[key] = json[key];
      }
   }
   return obj; // we finally return the instance
};

This requires your class to have setters for all its properties. This method is static, so you can use like this :

var instance = Obj.createFromJSON({"label":"MyLabel"});
var instance2 = Obj.createFromJSON('{"label":"MyLabel"}');
Share:
12,736

Related videos on Youtube

Cystack
Author by

Cystack

Updated on June 04, 2022

Comments

  • Cystack
    Cystack almost 2 years

    I have an app that allows users to generate objects, and store them (in a MySQL table, as strings) for later use. The object could be :

    function Obj() {
        this.label = "new object";
    }
    
    Obj.prototype.setLabel = function(newLabel) {
        this.label = newLabel;
    }
    

    If I use JSON.stringify on this object, I will only get the information on Obj.label (the stringified object would be a string like {label: "new object"}. If I store this string, and want to allow my user to retrieve the object later, the setLabel method will be lost.

    So my question is: how can I re-instantiate the object, so that it keeps the properties stored thanks to JSON.stringify, but also gets back the different methods that should belong to its prototype. How would you do that ? I was thinking of something along "create a blank object" and "merge it with the stored one's properties", but I can't get it to work.

    • humanityANDpeace
      humanityANDpeace over 10 years
      I applaud you @Cystack ! The notion of turning Javascript Objects (that are pure data attributes, no methods) is discussed here a lot. Yet this is the first question that I encounter that really focuses on objects, that are objects with methods. The JSON.stringify method normaly bears enough pitfalls (cyclic references, escaping/charsets, etc), so that most question focus on that and sadly very little I can find about this "automatic reinstanciating".
  • Cystack
    Cystack over 12 years
    I don't want to put code in JSON, I'm the just mentioning the fact that it's not storing the methods in any way. But I am open to any solution !
  • Cystack
    Cystack over 12 years
    what do you mean ? so that I store the method as a string in the object ? that defeats the purpose of the prototype, no ?
  • Cystack
    Cystack over 12 years
    very good ! this is the closest to what I thought. Now the only downside is that I spent hours not to include jQuery for various reasons, I would hate myself to include it just for that ^^ I'll look into the .extend function's code though
  • pimvdb
    pimvdb over 12 years
    @Cystack: There are numerous extend functions available in many libraries; you could also just lend one and copy it into your project. The underscore.js one is free of dependencies: documentcloud.github.com/underscore/underscore.js.
  • T.J. Crowder
    T.J. Crowder over 12 years
    "So far as I know, this means moving away from JSON..." Not really, reviver functions have been around for a long time.
  • pimvdb
    pimvdb over 12 years
    +1 Nice solution; perhaps in case setXXX is not available, just set it directly (since quite a number of properties may not need a separate setter function).
  • dnuttle
    dnuttle over 12 years
    I guess I did not understand the nature of the question. A reviver function allows you to use an existing function (not one in the JSON) to do things to the data as it's read, including building an object and adding your own code. This could mean that you could store property names that flag what functions should be added to the reconstituted object. But it doesn't store code in the JSON. Unless I'm missing something...if I am, please feel free to enlighten me.
  • Cystack
    Cystack over 12 years
    interesting suggestion, but this works only with your Foo "class". If you look at my Obj(), I cannot use this to revive the object, for the .labelproperty will be lost.
  • j-a
    j-a over 12 years
    not necessarily IN the object, in your DB, just add a table for methods and which object they belong to, send back normal json for the object and then the method strings and add them to the objects..
  • Cystack
    Cystack over 12 years
    see this based on your example : jsfiddle.net/cBBd4 the nifty property is lost
  • T.J. Crowder
    T.J. Crowder over 12 years
    @Cystack & pimvdb: The problem here is that it assumes that Obj can be called with no arguments, and as pimvdb points out, it bypasses the API of the type (which may be just fine, or not). In my view, you're much better off deferring those decisions to the type itself (which could, of course, just happily use extend if appropraite).
  • T.J. Crowder
    T.J. Crowder over 12 years
    @Cystack: Actually, I added a generalized reviver (and just replaced it with a better one). Re your fiddle: Sure, that's down to the Foo object's toJSON function, which needs to be sure to save appropriate properties (many objects will have properties that shouldn't be serialized). You can also do a generalized thing, as shown in my latest update.
  • humanityANDpeace
    humanityANDpeace over 10 years
    I am thankful for the displayed ideas. The notion to re-instantiate an object, so that serialization process ends again in objects that have methods is something useful. Besides the classic problem of circular references and JSON.stringify it is a necessary thing to store away real objects (=with methods) (not just data). Anyway having to manualy cite the objects name seems not yet the most perfect solution. What about this to get the constructor refernce automatically?
  • humanityANDpeace
    humanityANDpeace over 10 years
    Since the OP asked about the objects to have methods, when it comes to JSON, then I think the idea to use the replacer to store the methods in a String (which maybe should be escaped and or base64 to not brake JSON notation) and to revive the methods later. The suggestion may not be considered (nice, i.e. to have methods stored in a DB) but I think it links to the OP and the answer defnitively enriches the solution space, @j-a thank you!