8

I'm unclear how serialization/de-serialization is supposed to work on typed objects in JavaScript. For example, I have a "MapLayer" object that contains various members and arrays. I have written (but not yet tested) the following code to attempt to serialize it:

MapLayer.prototype.serialize = function() {
   var result = "{tileset:tilesets." + tilesets.getTilesetName(this.tileset) + ",columns:" + this.columns + ",rows:" + this.rows + 
   ",offsetX:" + this.offsetX + ",offsetY:" + this.offsetY + ",currentX:" + this.currentX + ",currentY:" + this.currentY + 
   ",scrollRateX:" + this.scrollRateX + ",scrollRateY:" + this.scrollRateY + ",virtualColumns:" + this.virtualColumns + ",virtualRows:" + this.virtualRows +
   ",tiles:\"" + this.encodeTileData2() + "\"";
   for(key in this)
   {
      if(this[key] instanceof Sprite)
         result += "," + key + ":" + this[key].serialize();
   }
   return result;
}

My question is, how is the resulting object supposed to get deserialized as a MapLayer object rather than as a generic Object. And how are all the Sprite instances supposed to get deserialized as sprites. Should I be using "new MapLayer()" instead of "{}"? Or am I simply supposed to include the prototype and constructor properties of the object in the serialization? Anything else I'm missing? Am I doing this a stupid way? There are 2 reasons I'm not using generic serialization/de-serialization code:

  1. I want to serialize the tile data in an optimized format rather than storing a base-10 string representation for each tile, and have them all delimited by commas.
  2. I don't want to serialize the tileset as an object that gets constructed as a new object during de-serialization, but rather as a reference to an existing object. Is that possible using code like I have proposed above?

Edit: Excuse my lack of proper terminology; JavaScript is one of my less expert languages. What I mean when I said "Typed Object" is an object with a constructor. In this example, my constructor is:

function MapLayer(map, tileset, columns, rows, virtualColumns, virtualRows, offsetX, offsetY, scrollRateX, scrollRateY, priority, tileData) {
   this.map = map;
   this.tileset = tileset;
   this.columns = columns;
   this.rows = rows;
   this.offsetX = offsetX;
   this.offsetY = offsetY;
   this.currentX = offsetX;
   this.currentY = offsetY;
   this.scrollRateX = scrollRateX;
   this.scrollRateY = scrollRateY;
   if(tileData.length < columns * rows * 2)
      this.tiles = DecodeData1(tileData);
   else
      this.tiles = DecodeData2(tileData);
   this.virtualColumns = virtualColumns ? virtualColumns : columns;
   this.virtualRows = virtualRows ? virtualRows : rows;
}

Edit 2: Taking the code from Šime Vidas' answer, I have added a related object called "Device":

function Device( description, memory ) {
    this.description = description;
    this.memory = memory;
}

function Person( name, sex, age, devices ) {
    this.name = name;
    this.sex = sex; 
    this.age = age;
    this.devices = devices;
}

Person.deserialize = function ( input ) {
    var obj = JSON.parse( input );
    return new Person( obj.name, obj.sex, obj.age, obj.devices );
};

var device = new Device( 'Blackberry', 64);
var device2 = new Device( 'Playstation 3', 600000);
var person = new Person( 'John', 'male', 25, [device, device2] );

var string = JSON.stringify(person);
console.log( string );

var person2 = Person.deserialize( string );
console.log( person2 );
console.log( person2 instanceof Person );

Now the question is how best to incorporate such dependent objects, because once again, the "type" (prototype?) of the object gets lost by JSON. Instead of running the constructor, why don't we simply change the serialize and the de-serialize functions to ensure that the object only needs to be constructed once like this instead of created and copied?

Person.prototype.serialize = function () {
    var obj = this; 
    return '({ ' + Object.getOwnPropertyNames( this ).map( function ( key ) {
        var value = obj[key];
        if ( typeof value === 'string' ) { value = '"' + value + '"'; }
        return key + ': ' + value;
    }).join( ', ' ) + ',"__proto__":Person.prototype})'; 
};

Person.deserialize = function ( input ) {
    return eval( input );
};

Edit 3: Another problem I have is that JSON doesn't seem to work in IE9. I'm using this test file:

<html>
<head>
<title>Script test</title>
<script language="javascript">
console.log(JSON);
</script>
</head>
</html>

And the console outputs:

SCRIPT5009: 'JSON' is undefined 
test.html, line 5 character 1

Edit 4: To correct the JSON problem I must include the correct DOCTYPE tag at the beginning.

BlueMonkMN
  • 25,079
  • 9
  • 80
  • 146

3 Answers3

8

For a start, here is a simple example of custom serialization / deserialization:

function Person( name, sex, age ) {
    this.name = name;
    this.sex = sex;
    this.age = age;
}

Person.prototype.serialize = function () {
    var obj = this;
    return '{ ' + Object.getOwnPropertyNames( this ).map( function ( key ) {
        var value = obj[key];
        if ( typeof value === 'string' ) { value = '"' + value + '"'; }
        return '"' + key + '": ' + value;
    }).join( ', ' ) + ' }';
};

Person.deserialize = function ( input ) {
    var obj = JSON.parse( input );
    return new Person( obj.name, obj.sex, obj.age );
};

Usage:

First, we create a new instance object:

var person = new Person( 'John', 'male', 25 );

Now, we serialize that object into a string using the Person.prototype.serialize method:

var string = person.serialize();

This will give use this string:

{ "name": "John", "sex": "male", "age": 25 }

Finally, we deserialize that string using the Person.deserialize static method:

var person2 = Person.deserialize( string );

Now, person2 is an instance of Person and contains the same property values as the original person instance.

Live demo: http://jsfiddle.net/VMqQN/


Now, while the Person.deserialize static method is required in any case (it uses JSON.parse internally, and invokes the Person constructor to initialize a new instance), the Person.prototype.serialize method on the other hand, is only needed if the built-in JSON.stringify static method doesn't suffice.

In my example above var string = JSON.stringify( person ) would get the job done too, so a custom serialization mechanism is not needed. See here: http://jsfiddle.net/VMqQN/1/ However, your case is more complex, so you'll need to define a custom serialization function.

Šime Vidas
  • 182,163
  • 62
  • 281
  • 385
  • I have updated the question. Why don't we set `__proto__` instead of calling the constructor? – BlueMonkMN Dec 30 '11 at 13:00
  • Another problem is, if I try to use JSON in IE, I get an error "JSON is undefined". – BlueMonkMN Dec 30 '11 at 13:27
  • @BlueMonkMN Because `__proto__` is non-standard and deprecated. It has never been implemented in IE (and never will be). The ECMAScript standard does not provide any means to set the prototype link of an object subsequently. ... Which version of IE are you using? The `JSON` object was added in IE8... – Šime Vidas Dec 30 '11 at 14:02
  • @BlueMonkMN Try `console.log( JSON )` in IE. You sould get `'[Object JSON]'` in IE's console (F12 -> Console tab)... – Šime Vidas Dec 30 '11 at 14:28
  • The console reports an error using the test HTML script I added to my question. – BlueMonkMN Dec 30 '11 at 15:29
  • 1
    @BlueMonkMN Ah that's because you didn't define a doctype. Add ` ` as the first line of your HTML document. If you fail to define a valid doctype, IE does into quirks mode... – Šime Vidas Dec 30 '11 at 15:35
  • OK, thanks for the answer and all your help. I just wanted some confirmation that JSON was not really "serialization for free" like you get with the binary serialization in .NET. If you want to do anything at all complex, you need to layer some other serialization code on top of it, it appears. BTW, will garbage collection efficiently clean up all those temporary objects that are constructed during deserialization? – BlueMonkMN Dec 30 '11 at 15:59
  • @BlueMonkMN With JavaScript, the rule is that an object exists in memory as long as there exists at least one reference to it. For example, inside the `Person.deserialize` function in my code above, a local variable `obj` is declared and the object returned by `JSON.parse` is assigned to it. Now, since the local variable is the only reference to that object, as soon as the local variable is destroyed (as soon as the function returns), the object will be garbage collected. – Šime Vidas Dec 30 '11 at 16:12
  • @ŠimeVidas while `__proto__` is non-standard, it appears `constructor` can accomplish a similar thing, and is more standard. What can you tell me about the idea of setting all the properties and then setting `constructor` to the function that constructed the object? Would that work? An instanceof test suggests it would in Chrome and IE. – BlueMonkMN Dec 31 '11 at 14:04
  • @BlueMonkMN I'm not sure what you mean. Take a look at this demo: http://jsfiddle.net/zmB7Y/ If `instanceof` works for you, please provide a demo. – Šime Vidas Dec 31 '11 at 14:32
  • @ŠimeVidas I can't make it work any more and I've lost the code that suggested it did work, so I must have made some error or stumbled across some obscure case that I can't reproduce. Sorry. – BlueMonkMN Dec 31 '11 at 15:20
  • JSON allows for custom serialization by adding a `toJSON` method to the constructor, which will not be serialized. As for deserialization, you can pass a custom function to the `JSON.parse` method as the second argument, which will return the deserialization from a passed in string. – Zev Spitz Nov 14 '12 at 21:46
1

If you look at the ST-JS (http://st-js.org) project, it allows to create your object graph in Java on the server side, serialize it in JSON, and deserialize it on the client side (Javascript) in a typed manner, i.e. the objects will be instantiated using their constructor and you can call methods on the created objects.

To use ST-JS you should write your client code is Java, that is converted almost one-to-one in Javascript.

The AJAX/JSON chapter on the home page of the site explains you how to parse a JSON string while keeping the type information.

LordOfThePigs
  • 11,050
  • 7
  • 45
  • 69
alex.c
  • 21
  • 1
  • I think the OP was asking about techniques to use for their existing codebase, rather than a different framework to migrate to. – Richard Marr Oct 26 '12 at 16:47
  • Yes you're somehow right. The problem I was pointing out is that in order to be able to deserialize in a generic way a graph of "typed" objects (like in Edit 2) you need to somehow store some metadata about your "typed objects", i.e. to tell to your deserializer that for example "devices" property of "Person" is an array of "Device". – alex.c Nov 20 '12 at 20:22
  • If you look at our [sources](https://github.com/st-js/st-js/blob/master/generator/src/main/resources/stjs.js) the parseJSON function (that one should be able to easily extract) uses a "static" field called "typeDescription" that is a simple map - that our framework generates, but that can be also created manually. – alex.c Nov 20 '12 at 20:31
0

My question is, how is the resulting object supposed to get deserialized as a MapLayer object rather than as a generic Object. And how are all the Sprite instances supposed to get deserialized as sprites.

I've made an npm module named esserializer to solve this problem: save JavaScript class instance values during serialization, in plain JSON format, together with its class name information:

const ESSerializer = require('esserializer');
const serializedText = ESSerializer.serialize(anInstanceOfClassMapLayer);

Later on, during the deserialization stage (possibly on another machine), esserializer can recursively deserialize object instance, with all Class/Property/Method information retained, using the same class definition:

const deserializedObj = ESSerializer.deserialize(serializedText, [MapLayer, Sprite]);
// deserializedObj is a perfect copy of anInstanceOfClassMapLayer

Should I be using "new MapLayer()" instead of "{}"? Or am I simply supposed to include the prototype and constructor properties of the object in the serialization?

Inside ESSerializer, class name information is included during serialization, and all prototype properties are re-constructed during deserialization.

I don't want to serialize the tileset as an object that gets constructed as a new object during de-serialization, but rather as a reference to an existing object.

Unfortunately, it is impossible. The serialization happens on one machine and the deserialization may happen on another computer -- anyway, this is what serialization/deserialization supposed to do. Computer would never know "an existing object" on another machine.

shaochuancs
  • 15,342
  • 3
  • 54
  • 62