Serializing Objects in Javascript

It's worth noting that this post is rather old at this point. I don't use the function listed in this post anymore and haven't for a rather long time. If you are using it and it's working for you, great! But as Ron in the comments sections points out, there are a few issues with regards to strings and special characters. I recommend following the link he posts if your needs merit a very robust version of JSON serialization. I'm leaving this post up though, as I think it's helpful for folks who want to understand the general concept of recursion and serialization.

Recently, in a personal project I'm working on, I came across a need to be able to represent any Javascript object as a string. This isn't a problem since just about every object in Javascript can be represented with JSON (Javascript Object Notation). Every modern browser can parse JSON for you easily enough through eval(..), and Gecko-based browsers even have the ability to reverse the process ("uneval" if you will) and give you back a string representation of an object through a call to .toSource().

If you need this ability in any other browser though, you're gonna have to write it yourself. I needed this ability, so I wrote it (and posted it here for your enjoyment!)

Gecko-based browsers, .toSource():

Gecko-based browsers provide a handy function: .toSource() that you can call on any object in your Javascript code to get back a JSON-like representation of that object.

function Cat(name, age)
{
   this.Name = name;
   this.Age = age;
   this.Speak = function() { alert('Meow!'); };
}

var garfield = new Cat('Garfield', 5);
alert(garfield.toSource());

/* garfield.toSource() yields:
({Name:"Garfield", Age:5, Speak:(function () {alert("Meow!");})})
*/

 

Pretty simple right? You have an object; you want a string. Just invoke the object's .toSource() function.

Serializing objects in other browsers:

Serializing an object manually (as is required by non Gecko-based browsers) requires a bit of recursion. Simple types like integers, booleans, and even functions are trivial to represent as strings. Objects though, are more complicated because they can contain simple types or custom objects (which would need to be serialized themselves). Those "inner" objects could in turn, contain more custom objects, which would also need to be serialized, and this pattern could (theoretically) go on forever.

In practice of course, this pattern will (had better) come to an end. And we can leverage that fact to write a recursive function that will return a string representation (in JSON format) of a given object.

The serialize(..) function:

First the code, then the explanation.

function serialize(_obj)
{
   // Let Gecko browsers do this the easy way
   if (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 + '\'';
         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 += serialize(_obj[i]) + ','; }
            str += serialize(_obj[i]) + ']';
         }
         else
         {
            str = '{';
            var key;
            for (key in _obj) { str += key + ':' + serialize(_obj[key]) + ','; }
            str = str.replace(/\,$/, '') + '}';
         }
         return str;
         break;

      default:
         return 'UNKNOWN';
         break;
   }
}

Explaining a recursive function can be difficult, but I'll give it shot:

The function accepts just one parameter: the object (_obj) to be serialized. If you'll remember, I mentioned previously that simple types (string, boolean, number, etc...) were trivial because they all have an obvious string representation already. Complex types though, are more difficult because they can be made up of additional complex types, which in turn could be made up of additional complex types (and so on).

Of course, this pattern will eventually end; ultimately, everything is made up of simple types that have a string representation. The trick is figuring out how to traverse through this maze of "types within types." Recursion (simply stated: a function that calls itself) is perhaps the easiest way to solve this "types within types" problem.

Recursive functions always have a termination case -- something which causes the function to stop calling itself. Otherwise, the function would go into an infinite loop. In our function, there are actually four different cases in which the serialize(..) doesn't need to call itself:

  1. typeof _obj is a number
  2. typeof _obj is a boolean
  3. typeof _obj is a function
  4. typeof _obj is a string

If any of the above four conditions are met, returning a string representation is trivial, so we simply do it.

You'll notice that strings are treated separately from the other 3 types. You'd think strings would be the most trivial case, but actually, there is one thing we must do before returning the "string representation" of _obj when it is of type string: wrap it in quotes. We need to do this, because JSON expects it, and if we ever want to be able to eval(..) the result of a serialize(..) call, we'll need these quotes.

The only other case to deal with is when _obj is of type object. Within this case though, there are two "sub-cases" we need to deal with. The first is when _obj is an Array, or when it has a .callee property (more on that later). The second is well... anything else.

Basic Object Types

Your every-day, run-of-the-mill, object in Javascript can be represented as JSON with:

{ key1: val1, key2: val2, ... }

Where the keys are strings and the vals can be any simple type, or some custom object you've dreamed up. The logic I've used is to simply loop through a given object's keys and build a string of comma delimited, serialized key/value pairs that are wrapped in { and }.

Notice I said serialized key/value pairs. Here, our function is calling itself as it builds the object representation. This ensures that any objects within the object being serialized will also be serialized. If we didn't do this, we'd end up with a lot of strings that looked (something) like this:

{ key1: [object Object], key2: [object Object], etc... }

And that's clearly not what we want. We want those inner objects to be serialized as well, and that's what the recursive nature of our function will take care of for us.

Arrays

When _obj happens to be, not just any object, but more specifically, an Array, we have a better way of representing that as a string:

[ val1, val2, val3, ... ]

The logic I used here is to simply iterate through the array building a comma delimited list of serialized values, wrapped in [ and ]. Arrays in Javascript already have a .toString() function, but we can't use it here; if the Array contains objects, then the result of the Array 's .toString() could end up something like:

[ [object Object], [object Object], etc... ]

Again, not what we want, so we need to make sure we recursively serialize all of the elements in the array.

Arguments (the .callee "gotcha")

There's one bit of code I haven't discussed yet and it deals with the (possible) .callee property of the passed in _obj. It turns out that .toSource() (native function used by Gecko-based browsers) doesn't do anything very useful when called on an arguments object.

The arguments object is an array-like (but not an Array) object that is automatically available within the scope of every function. Unfortunately, no matter what is contained within that arguments object, calling .toSource() on it will always return "({})"

In order to be able to serialize an arguments object then, we need some way to detect it and then treat it like an array. The .callee property is a good choice because arguments objects have it, but other objects (to the best of my knowledge) do not.

In the case of the serialize(..) function, I decided to use the native .toSource() function whenever it was available, unless the object were an arguments object, in which case, I send Gecko-based browsers down the same path as all other browsers for serialization.

Finally, just for good measure, I've added a default case which returns the string UNKNOWN to handle a situation where no other cases applied. Of course, we probably don't want UNKNOWN showing up in the our serialized strings, but it probably won't do much (immediate) harm if it ever does show up, and its presence would be a helpful indicator that there is some case not being met (that probably needs to be).

Conclusion:

What would you ever use a function like this for? Well, In my case, I wanted to use a Javascript object as the key for a hash, but if I tried to do that, Javascript would just represent all of my (different) objects as the same string: [object Object], which wouldn't do me any good. By serializing the object, I can then use that serialized representation as a key in the hash.

It's worked well for my need so far, but I haven't tested it a great deal. If you find any bugs or short-comings, please let me know.

As always, comments are welcome.

32 Responses

  1. Jason Says:

    Just what I was looking for! I've been looking for a way to serialize functions (as functions are actually objects in Javascript), however my first attempt using Douglas Crockford's json2.js library (http://www.json.org/json2.js) didn't always work properly when serializing functions (though it works fine for serializing and deserializing other Javascript data structures).
    Your serialize function has nicely stepped into the breach!

  2. sstchur Says:

    Jason,

    Glad it worked for you! Please let me know if you encounter any problems with it.

  3. Pongi Says:

    Hey. I tried your function and it worked well in Firefox, but I ran into problems within Safari. Did you test this browser? Looks like it hangs on the first test, when it tries to employ the .toSource method.

  4. sstchur Says:

    Pongi,

    Thanks for pointing this out. I will double check later today and post back. I admit that back when I wrote this post I was not very good about checking in Safari. I have access to a Mac these days though, so I do it much more often lately. I'll give it a go and report back.

  5. sstchur Says:

    Pongi,

    I just tested Safari3/Win with a simple Cat object and it worked:

    var c = new Cat('Garfield', 5);
    alert(serialize(c));

    // yields: {Name:'Garfield',Age:5,Speak:function () { alert("Meow!"); }}

    I will try Safari2/Mac a little later. Do you have any additional information on what part of the code you think is causing Safari trouble? Which version of Safari by the way?

  6. Stephen Stchur Says:

    Ok, I tested this in Safari2 and 3 for Mac, and I had a problem in Safari2. It seems like some sort of an encoding issue because I copied and pasted the code from my blog into TextEdit and Safari kept barking about "parsing errors" (even though there were none).

    So I tried creating a brand new file on my Windows machine and uploading it to my server (Linux) and accessing that from Safari2/Mac, and then everything worked.

    See if this works for you in Safari: http://blog.stchur.com/blogcode/tmp/serialize.html

  7. fotonics Says:

    The JavaScript function toSource() does not produce double quotes around property names. It is fine in most cases but php's json_decode() function expects the double quotes. So I had to comment out the toSource() section and use the recursive function instead. Also I changed the single quotes you use to double quotes around all property values, and do a string replacement to escape all double quotes in a string.

  8. sstchur Says:

    @fotonics:

    I wasn't aware of the issue with PHP's json_decode() function. Thanks for pointing that out — I'm sure it will be helpful for more than one person down the road trying to use this function in concert with some PHP script.

  9. aky Says:

    in first not bad adding this lines

    if(_obj===null) return 'null';
    if(_obj===undefined)return 'undefined';

    Regard from Russia

  10. sstchur Says:

    Aky,

    Not at all a bad suggestion! Thanks.

    -Steve

  11. amit Says:

    hi buddy,
    thnx a lot… that is what i was looking all throughtout internet.
    It was so informative as well as usefull (to be directly used :p )
    thnx a lot once again…

    Amit

  12. zeeman Says:

    Hi!
    Thanks a lot for this nice piece of code!

  13. Ross Says:

    Hi sstchur
    Congratulations, very nice, very tight code.
    We are using it for a new tool that we will be releasing very soon.
    I can safely promise a serious testing of your code.
    If you email me, I can give you a preview.

  14. Jarek Says:

    Hi,

    Quite nice piece of code but unfortunatelly I encountered a problem.

    The problem with Safari 3.1 (3.1.2 to be precise) on Mac is the following:

    var a=[1,2,3];
    a.constructor === Array
    => false

    unfortunatelly:
    typeof a.callee !== 'undefined'
    => false

    so, this won't translate an array properly.

    One of possible solutions (a bit ugly one):
    a.constructor.toString().indexOf('Array') != -1
    => true

    so, changing following line:
    if (_obj.constructor === Array || typeof _obj.callee !== 'undefined')
    to:
    if (_obj.constructor === Array || typeof _obj.callee !== 'undefined' || _obj.constructor.toString().indexOf('Array') != -1)

    should solve the problem.

    Regards

  15. sstchur Says:

    Thanks Jarek! I will make a note of this, as I think this affects me in more than just this piece of code.

  16. Oliver Says:

    Hi Stephen,

    very nice and useful code and exactly, what I'm looking for! Thanks a lot for contributing.

    As Jarek mentioned before, there are problems with the correct detection of Arrays. My suggestion for solving this is using the property length which does not exist in Object().
    So my piece of code looks like:
    if (theObj.constructor === Array
    || theObj.length
    || typeof(theObj.callee) !== 'undefined')

    Regards from munich,

    Oliver

  17. kyb Says:

    To do the string thing correctly, you need to escape not just any quotes that are in there, but also any /s.
    I recommend something like

    var stringify_string = function(str) {
    str = str.replace(/\\/g, "\\\\");
    str = str.replace(/\"/g, "\\\"");
    return "\""+str+"\"";
    };

    The code you provide also, when evaluated, creates an object that is not of the right type. You can get around this in some situations on firefox by setting the __proto__ property – but that's only if you have a name that it should be set to.

    Other problems with general usage are loops, or equivalent objects. If an object or its descendant contains a reference to itself, this method will loop forever. Equal objects is a pain as well – if there are two references to the same object, when you deserialise, they will no longer be equal to each other. You can fix using something like this:

    {bob: (tmp={key: 1}), alf: tmp}

    When that is evaluated, you end up with a bob and alf being === each other. Of course, other object literal notation parsers may not cope with that, and you have set a global there.

    Fixing loops is a real pain – you need to keep a list of all objects that you've dealt with so far, and use references to them if they are the same as an earlier one.

  18. sstchur Says:

    @kyb:

    Some good points. Take the code for what it's worth… not appropriate for every single possible scenario I guess.

  19. Justin Hernandez Says:

    Thanks so much for this function. Works great and compact too!

  20. Chris McKenzie Says:

    Serializing arguments:

    alert(Array.prototype.slice.apply(arguments).toString());

  21. ADM Blog » Serialize JavaScript object to JSON Says:

    [...] all I could find online were scripts and jquery plugins for serializing a html form. Then, I found this, a script that takes advantage of the .toSource() method available in Gecko-based browsers and for [...]

  22. sstchur Says:

    That's why the blog is here offering public code snippets ;-) Contribute. Code it up and post it, so everyone can benefit!

  23. Woody Says:

    Very useful – took all the pain out of throwing an array of objects between pages ;) +props

  24. Himanshu Says:

    How to serialize and deseialize 2d array like

    INternalID NAME AGE
    100 A 26
    101 B 32
    102 C 89

  25. javascript 序列化 | 张经纬的博客 Says:

    [...] 来源:http://blog.stchur.com/2007/04/06/serializing-objects-in-javascript/–Serializing Objects in Javascript [...]

  26. Nick Kaal Says:

    Nice,

    It is just what i was looking for :)

    tyvm

  27. ajuc Says:

    Last problem is with recursive structures :)

    I've encountered this when serializing Box2dWeb b2World object. It throws (obviously) Maximum call stack size exceeded Exception.

  28. sstchur Says:

    What is a Box2dWeb b2World object?

  29. Ron Cemer Says:

    Your string serialization isn't properly escaping special characters. For example, quotes inside the string should be prefixed with backslashes; backslashes in the string should be replaced with double backslashes; characters outside the ASCII character range should (probably) be replaced with their hexadeximal unicode equivalent (\xNNNN). The way it is now, all you have to do to break it is to include a single quote within a string anywhere in the object tree. The result will be unparseable JavaScript due to syntax errors. Try this instead, which is straight from the horse's moutn (download links at the bottom of the page): http://www.json.org/js.html

  30. Your Fast Catalog | Brad Czerniak Says:

    [...] It's over 150K all by itself, and it includes the sizzle selector library and a bunch of other utilities. Which makes one wonder — why not just use jQuery? Moreover, why does a file that loads a [...]

  31. Henrique Says:

    Ohhh! Thank you so much!!!!!

  32. Flavio Camus Says:

    Thank you so much, i was looking for a simple solution, an this is great!

Got something to say?

Please note: Constructive criticism is welcome. Rude or vulgar comments however, are not and will be removed during moderation.