The pursuit of JSON.stringify
TL;DR: In this post, I briefly explored the humble JSON.stringify
. In that sense it is not an exhaustive guide for that. Thereafter, a @decorator
+ Symbol
based solution is presented to elegantly mark properties to be excluded from JSON.
Few days back at work, I was getting my code reviewed from one of my colleagues. The code that was being reviewed was written for a requirement, which involves serializing a JavaScript object as JSON for persisting in database/backend store. The target objects for serialization are instances of a particular class that contains properties, which need to be ignored for serialization. For example, the values of those properties does not needed to be persisted or could not be serialized, due to complex circular reference. As an example, let us consider the following class.
And when an instance of Target
is serialized we want it to be {"x": value_of_x}
instead of {"x": value_of_x, "ignored": "test"}
.
Now you maybe thinking at this point that is easy peasy. A replacer parameter can certainly be used to ignore those properties, either an array to whitelist the properties…
… or a full-fledged replacer function for more involved cases.
The problems with this replacer approach are the following. With the addition of future ignored or non-ignored properties we might need to update the replacer. Moreover, the logic of ignoring the properties is not at all encapsulated with the class. I know you are ready with a toJSON
solution to fix the encapsulation that may look something like below.
However, things get more complicated when this class is inherited, and new properties, ignored or otherwise, are introduced by the derived class. If we walk this road of toJSON
, we may end up having multiple overridden versions of this method in multiple classes, which does not seem like a clean solution.
What I came up with instead is a solution involving Symbol
. As Symbol
-keyed properties are ignored completely during serialization (refer MDN), using a symbol for the “ignored” properties seemed like perfect choice.
Now you can imagine what happens if there are multiple properties to ignore for JSON; we end up with lot of symbol, one for each “ignored” property. And again that is not a particularly clean solution.
My reviewer colleague, being a C# pro dev, reminded me of this nice JsonIgnoreAttribute
from Json.NET which when applied on a property, prevents serialization of that property value.
@decorator to rescue
@decorator
s can be considered as somewhat similar to the idea of Attributes in C#, in the sense that both embodies the idea of decorator pattern. Therefore, the alternative idea for ignoring properties for JSON, is to write a decorator that facilitates that, and apply that decorator on the properties, to be ignored. With this notion, I came up with the following as my first attempt.
Attempt#1
What we have here is a property decorator, that takes the prototype of the class, and the name of the target property as arguments. Then it returns a property descriptor, that is in turn applied on the property of the class. If you are interested, I would encourage you to take a look at the JavaScript source generated by TypeScript compiler. The code generated for decorator looks something like below (your TypeScript version may produce a slightly different version).
The key thing to note here is the Object.defineProperty(target, key, r)
on the return
statement. This means that the property descriptor returned from the decorator is applied on the property. The property descriptor redefines the property with get
/set
accessors, backed by a new underlying property named as _${key}
(i.e. when the decorator is applied on a property ignored
it creates the backing property as _ignored
). Furthermore, the set
accessor defines the new property as non-enumerable (enumerable: false
), which ensures that when an instance of the target class serialized, the backing property is never included in that (you may verify this from the repl below).
Moreover, as the target
in Object.defineProperty(target, key, r)
is the prototype of the class, the property is (re)defined for the Class.prototype
(for our example, it is Target.prototype
). This also causes the exclusion of this property from the serialized JSON. This may not come as a surprise, due to the fact that the get
accessors are also not serialized, as those are defined the very same way on Class.prototype
(check the example below). The behavior is in accordance with the specs of JSON.stringify
(if you are interested, traverse the specs till EnumerableOwnNames from SerializeJSONObject).
Attempt#2 (final)
Though the previous attempt work, it can be made much simpler. In fact when the backing property is made Symbol
-keyed, we don’t have to even bother about making the backing property non-enumerable (in this context). As previously indicated, all Symbol
-keyed properties are by default ignored by JSON.stringify
. Applying this notion, we have the final version of this decorator below.
This looks a lot simpler than the previous version. The property descriptor ensure that the property remains writable, but as the backing property is Symbol
-keyed, it is automatically excluded from the JSON. Moreover, it is more performant than the previous (next section).
Performance comparison
To formally compare the performance of the 2 variants, I cooked up the following code. In this code, there are 2 target classes, and each variant of the decorator is applied on a property of each class. Then a similar code fragment for each class is tested in iteration. The iteration ranges from 10 to 1e+07. The code fragment instantiates an object of the target class, reads and writes the ignored property, and lastly performs a JSON.stringify
on the object.
You can run the code, use your favorite visualization tool to generate charts, and see if it closely matches the results presented in this post. For convenience, I am adding the result of a sample run.
Iteration | Variant#1 Total (d1) |
Variant#1 Average |
Variant#2 Total (d2) |
Variant#2 Average |
d1/d2 |
---|---|---|---|---|---|
10 | 0.9774 | 0.0977 | 0.1089 | 0.0109 | 8.971 |
100 | 2.074 | 0.0207 | 0.1437 | 0.0014 | 14.434 |
1000 | 2.6187 | 0.0026 | 1.5786 | 0.0016 | 1.6589 |
1e+06 | 1741.3541 | 0.0017 | 870.619 | 9e-04 | 2.0001 |
5e+06 | 8770.4606 | 0.0018 | 4104.5207 | 8e-04 | 2.1368 |
1e+07 | 17333.5307 | 0.0017 | 8173.5535 | 8e-04 | 2.1207 |
From this result, it is clear that the second variant of the decorator is more performant than the first one. And in general, the first one takes 2x more time than the second one.
Summary
This post has explored briefly the JSON.stringify
. For a long time it never crossed my the amount of complexity that is involved behind this humble function. Moreover, it also presented a solution based on @decorator
and Symbol
to elegantly mark properties to be excluded from the serialized JSON. My personal opinion is that is a far better way, in this context, than applying either replacer or toJSON
.
Hope this helps.