Reputation: 12389
I have many JavaScript objects in my application, something like:
function Person(age) {
this.age = age;
this.isOld = function (){
return this.age > 60;
}
}
// before serialize, ok
var p1 = new Person(77);
alert("Is old: " + p1.isOld());
// after, got error Object #<Object> has no method 'isOld'
var serialize = JSON.stringify(p1);
var _p1 = JSON.parse(serialize);
alert("Is old: " + _p1.isOld());
See in JS Fiddle.
My question is: is there a best practice/pattern/tip to recover my object in same type it was before serialization (instances of class Person, in this case)?
Requirements that I have:
Upvotes: 89
Views: 184867
Reputation: 816364
JSON has no functions as data types. You can only serialize strings, numbers, objects, arrays, and booleans (and null
)
You could create your own toJson
method, only passing the data that really has to be serialized:
Person.prototype.toJson = function() {
return JSON.stringify({age: this.age});
};
Similar for deserializing:
Person.fromJson = function(json) {
var data = JSON.parse(json); // Parsing the json string.
return new Person(data.age);
};
The usage would be:
var serialize = p1.toJson();
var _p1 = Person.fromJson(serialize);
alert("Is old: " + _p1.isOld());
To reduce the amount of work, you could consider to store all the data that needs to be serialized in a special "data" property for each Person
instance. For example:
function Person(age) {
this.data = {
age: age
};
this.isOld = function (){
return this.data.age > 60;
}
}
then serializing and deserializing is merely calling JSON.stringify(this.data)
and setting the data of an instance would be instance.data = JSON.parse(json)
.
This would keep the toJson
and fromJson
methods simple but you'd have to adjust your other functions.
Side note:
You should add the isOld
method to the prototype of the function:
Person.prototype.isOld = function() {}
Otherwise, every instance has it's own instance of that function which also increases memory.
Upvotes: 83
Reputation: 598
json-stash can serialize anything you can define a serializer for.
import { stash, unstash, addSerializers } from 'json-stash'
function Person(age) {
this.age = age;
this.isOld = function () {
return this.age > 60;
};
}
var p1 = new Person(77);
expect(p1.isOld()).toBe(true);
// add a serializer
addSerializers({ type: Person, save: (p) => [p.age] });
// serialize to JSON
var serialize = stash(p1);
expect(serialize).toBe('{"$type":"Person","data":[77]}');
// deserialize to Person object
var _p1 = unstash(serialize);
expect(_p1).toBeInstanceOf(Person);
expect(_p1.isOld()).toBe(true);
Upvotes: 1
Reputation: 93
You can create an empty instance of of your class and assign values to it with Object.assign
.
let p1 = new Person(77);
let serialized = JSON.stringify(p1);
let deserialized = Object.assign(new Person(), JSON.parse(serialized))
Upvotes: 2
Reputation: 16226
I've made an npm module named esserializer to solve this problem: save JavaScript class object values during serialization, in plain JSON format, without storing any functions. During the serialization, the only overhead incurred is saving the class names information. Thus, the disk usage is optimized.
Later on during the deserialization stage, esserializer can recursively deserialize object instance, with all types/functions information retained. It works in both browser and Node.js environment.
In the OP's case, the code would be pretty easy:
var ESSerializer = require('esserializer');
function Person(age) {
this.age = age;
this.isOld = function (){
return this.age > 60;
}
}
// before serialize, ok
var p1 = new Person(77);
alert("Is old: " + p1.isOld());
// serialize
var serializedText = ESSerializer.serialize(p1);
//...do something, or send the above serializedText to another JavaScript environment.
// deserialize
var deserializedObj = ESSerializer.deserialize(serializedText, [Person]);
alert("Is old: " + deserializedObj.isOld());
the deserializedObj
is a Person
instance, which contains all values/functions/superclasses information.
Wish it could help.
Upvotes: 2
Reputation: 2208
I tried to do this with Date
with native JSON
...
function stringify (obj: any) {
return JSON.stringify(
obj,
function (k, v) {
if (this[k] instanceof Date) {
return ['$date', +this[k]]
}
return v
}
)
}
function clone<T> (obj: T): T {
return JSON.parse(
stringify(obj),
(_, v) => (Array.isArray(v) && v[0] === '$date') ? new Date(v[1]) : v
)
}
What does this say? It says
$date
, if you want it more secure.class Klass {
static fromRepr (repr: string): Klass {
return new Klass(...)
}
static guid = '__Klass__'
__repr__ (): string {
return '...'
}
}
This is a serializable Klass, with
function serialize (obj: any) {
return JSON.stringify(
obj,
function (k, v) { return this[k] instanceof Klass ? [Klass.guid, this[k].__repr__()] : v }
)
}
function deserialize (repr: string) {
return JSON.parse(
repr,
(_, v) => (Array.isArray(v) && v[0] === Klass.guid) ? Klass.fromRepr(v[1]) : v
)
}
I tried to do it with Mongo-style Object ({ $date }
) as well, but it failed in JSON.parse
. Supplying k
doesn't matter anymore...
BTW, if you don't care about libraries, you can use yaml.dump
/ yaml.load
from js-yaml
. Just make sure you do it the dangerous way.
Upvotes: 0
Reputation: 2186
I've added yet another JavaScript serializer repo to GitHub.
Rather than take the approach of serializing and deserializing JavaScript objects to an internal format the approach here is to serialize JavaScript objects out to native JavaScript. This has the advantage that the format is totally agnostic from the serializer, and the object can be recreated simply by calling eval().
https://github.com/iconico/JavaScript-Serializer
Upvotes: 2
Reputation: 9884
I am the author of https://github.com/joonhocho/seri.
Seri is JSON + custom (nested) class support.
You simply need to provide toJSON
and fromJSON
to serialize and deserialize any class instances.
Here's an example with nested class objects:
import seri from 'seri';
class Item {
static fromJSON = (name) => new Item(name)
constructor(name) {
this.name = name;
}
toJSON() {
return this.name;
}
}
class Bag {
static fromJSON = (itemsJson) => new Bag(seri.parse(itemsJson))
constructor(items) {
this.items = items;
}
toJSON() {
return seri.stringify(this.items);
}
}
// register classes
seri.addClass(Item);
seri.addClass(Bag);
const bag = new Bag([
new Item('apple'),
new Item('orange'),
]);
const bagClone = seri.parse(seri.stringify(bag));
// validate
bagClone instanceof Bag;
bagClone.items[0] instanceof Item;
bagClone.items[0].name === 'apple';
bagClone.items[1] instanceof Item;
bagClone.items[1].name === 'orange';
Hope it helps address your problem.
Upvotes: 6
Reputation: 31
I had a similar problem and since I couldn't find a sufficient solution, I also created a serialization library for javascript: https://github.com/wavesoft/jbb (as a matter of fact it's a bit more, since it's mainly intended for bundling resources)
It is close to Binary-JSON but it adds a couple of additional features, such as metadata for the objects being encoded and some extra optimizations like data de-duplication, cross-referencing to other bundles and structure-level compression.
However there is a catch: In order to keep the bundle size small there are no type information in the bundle. Such information are provided in a separate "profile" that describes your objects for encoding and decoding. For optimization reasons this information is given in a form of script.
But you can make your life easier using the gulp-jbb-profile
(https://github.com/wavesoft/gulp-jbb-profile) utility for generating the encodeing/decoding scripts from simple YAML object specifications like this:
# The 'Person' object has the 'age' and 'isOld'
# properties
Person:
properties:
- age
- isOld
For example you can have a look on the jbb-profile-three
profile.
When you have your profile ready, you can use JBB like this:
var JBBEncoder = require('jbb/encode');
var MyEncodeProfile = require('profile/profile-encode');
// Create a new bundle
var bundle = new JBBEncoder( 'path/to/bundle.jbb' );
// Add one or more profile(s) in order for JBB
// to understand your custom objects
bundle.addProfile(MyEncodeProfile);
// Encode your object(s) - They can be any valid
// javascript object, or objects described in
// the profiles you added previously.
var p1 = new Person(77);
bundle.encode( p1, 'person' );
var people = [
new Person(45),
new Person(77),
...
];
bundle.encode( people, 'people' );
// Close the bundle when you are done
bundle.close();
And you can read it back like this:
var JBBDecoder = require('jbb/decode');
var MyDecodeProfile = require('profile/profile-decode');
// Instantiate a new binary decoder
var binaryLoader = new JBBDecoder( 'path/to/bundle' );
// Add your decoding profile
binaryLoader.addProfile( MyDecodeProfile );
// Add one or more bundles to load
binaryLoader.add( 'bundle.jbb' );
// Load and callback when ready
binaryLoader.load(function( error, database ) {
// Your objects are in the database
// and ready to use!
var people = database['people'];
});
Upvotes: 1
Reputation: 71
I had the exact same problem, and written a small tool to do the mixing of data and model. See https://github.com/khayll/jsmix
This is how you would do it:
//model object (or whatever you'd like the implementation to be)
var Person = function() {}
Person.prototype.isOld = function() {
return this.age > RETIREMENT_AGE;
}
//then you could say:
var result = JSMix(jsonData).withObject(Person.prototype, "persons").build();
//and use
console.log(result.persons[3].isOld());
It can handle complex objects, like nested collections recursively as well.
As for serializing JS functions, I wouldn't do such thing because of security reasons.
Upvotes: 2
Reputation: 16867
I wrote serialijse because I faced the same problem as you.
you can find it at https://github.com/erossignon/serialijse
It can be used in nodejs or in a browser and can serve to serialize and deserialize a complex set of objects from one context (nodejs) to the other (browser) or vice-versa.
var s = require("serialijse");
var assert = require("assert");
// testing serialization of a simple javascript object with date
function testing_javascript_serialization_object_with_date() {
var o = {
date: new Date(),
name: "foo"
};
console.log(o.name, o.date.toISOString());
// JSON will fail as JSON doesn't preserve dates
try {
var jstr = JSON.stringify(o);
var jo = JSON.parse(jstr);
console.log(jo.name, jo.date.toISOString());
} catch (err) {
console.log(" JSON has failed to preserve Date during stringify/parse ");
console.log(" and has generated the following error message", err.message);
}
console.log("");
var str = s.serialize(o);
var so = s.deserialize(str);
console.log(" However Serialijse knows how to preserve date during serialization/deserialization :");
console.log(so.name, so.date.toISOString());
console.log("");
}
testing_javascript_serialization_object_with_date();
// serializing a instance of a class
function testing_javascript_serialization_instance_of_a_class() {
function Person() {
this.firstName = "Joe";
this.lastName = "Doe";
this.age = 42;
}
Person.prototype.fullName = function () {
return this.firstName + " " + this.lastName;
};
// testing serialization using JSON.stringify/JSON.parse
var o = new Person();
console.log(o.fullName(), " age=", o.age);
try {
var jstr = JSON.stringify(o);
var jo = JSON.parse(jstr);
console.log(jo.fullName(), " age=", jo.age);
} catch (err) {
console.log(" JSON has failed to preserve the object class ");
console.log(" and has generated the following error message", err.message);
}
console.log("");
// now testing serialization using serialijse serialize/deserialize
s.declarePersistable(Person);
var str = s.serialize(o);
var so = s.deserialize(str);
console.log(" However Serialijse knows how to preserve object classes serialization/deserialization :");
console.log(so.fullName(), " age=", so.age);
}
testing_javascript_serialization_instance_of_a_class();
// serializing an object with cyclic dependencies
function testing_javascript_serialization_objects_with_cyclic_dependencies() {
var Mary = { name: "Mary", friends: [] };
var Bob = { name: "Bob", friends: [] };
Mary.friends.push(Bob);
Bob.friends.push(Mary);
var group = [ Mary, Bob];
console.log(group);
// testing serialization using JSON.stringify/JSON.parse
try {
var jstr = JSON.stringify(group);
var jo = JSON.parse(jstr);
console.log(jo);
} catch (err) {
console.log(" JSON has failed to manage object with cyclic deps");
console.log(" and has generated the following error message", err.message);
}
// now testing serialization using serialijse serialize/deserialize
var str = s.serialize(group);
var so = s.deserialize(str);
console.log(" However Serialijse knows to manage object with cyclic deps !");
console.log(so);
assert(so[0].friends[0] == so[1]); // Mary's friend is Bob
}
testing_javascript_serialization_objects_with_cyclic_dependencies();
Upvotes: 9
Reputation: 10099
The browser's native JSON API may not give you back your idOld function after you call JSON.stringify, however, if can stringify your JSON yourself (maybe use Crockford's json2.js instead of browser's API), then if you have a string of JSON e.g.
var person_json = "{ \"age:\" : 20, \"isOld:\": false, isOld: function() { return this.age > 60; } }";
then you can call
eval("(" + person + ")")
, and you will get back your function in the json object.
Upvotes: 3