A JSON Serializer with Referential Integrity

Below is a JSON serializer implemented in JavaScript, along with an automated test of that serializer.

When run, the program creates a complex test object, serializes it to a string in JSON format, deserializes that string back to an object, re-serializes the deserialized object back to a string, compares the original serialized string to the re-serialized string, and, if the two strings are identical, prints a confirmation message to the debugging console to that effect.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

This serializer provides some enhancements over other serializers I have written and used in previous projects. First, the code is somewhat cleaner. Second, the actual objects being serialized are not modified in any way during the serialization process–instead, a tree of separate “SerializationNode” objects is built that shadows the structure of the original object and its descendant objects. Third, this serializer does not require a static list of “known types” to be specified beforehand in order to assign class prototypes to generic deserialized objects, instead using the “eval” function to obtain these prototypes dynamically when needed. Fourth, this serializer can serialize objects that contain circular references, which is something that even the built-in JSON.stringify() can’t do by itself.

Fifth, and most significantly, this serializer maintains “referential integrity” of the serialized objects. That is, if objects A and B both reference a single object C before the serialization, then the subsequently deserialized versions of A and B will both reference the same deserialized instance of object C, rather than each getting their own copy of C. This breaking of referential integrity has played havoc with the architecture of past projects, requiring me to completely redesign otherwise working systems to support serialization. Hopefully this new serializer will make that sort of thing unnecessary.


<html>
<body>
<script type="text/javascript">

// main

function testSerializer()
{
	// Create a complex object to serialize.
	var one = new SerializationTestObject
	(
		true, // booleanValue
		1, // numberValue
		"One", // stringValue,
		function (a, b)
		{
			return a - b
		},
		// arrayValue
		[
			new SerializationTestObject
			(
				true,
				2, 
				"Two",
				null, // functionValue
				[] // arrayValue
			),

			new SerializationTestObject
			(
				true,
				3, 
				"Three",
				null, // functionValue
				[] // arrayValue
			),
		]
	);

	var four = new SerializationTestObject
	(
		true,
		4, 
		"Four",
		null, // functionValue
		null // arrayValue
	);

	// Create two references to a single object.
	
	one.arrayValue[0].arrayValue.push(four);
	one.arrayValue[1].arrayValue.push(four);	

	// Create a circular reference.
	one.arrayValue.push(objectToSerialize);

	var serializer = new Serializer
	([
		// known types
		SerializationTestObject
	]);

	var objectToSerialize = one;
	var objectSerialized = serializer.serialize(objectToSerialize);
	var objectDeserialized = serializer.deserialize(objectSerialized);
	var objectReserialized = serializer.serialize(objectDeserialized);

	if (objectSerialized != objectReserialized)
	{
		console.log("Test failed!");
	}
	else
	{
		console.log("Test passed!");
	}
}

// classes

function SerializationTestObject
(
	booleanValue, numberValue, stringValue, functionValue, arrayValue
)
{
	this.booleanValue = booleanValue;
	this.numberValue = numberValue;
	this.stringValue = stringValue;
	this.functionValue = functionValue;
	this.arrayValue = arrayValue;
}
{
	SerializationTestObject.prototype.add = function(a, b)
	{
		// This is part of the prototype,
		// and thus should not be serialized!
		return a + b;
	}
}

function Serializer()
{
	// do nothing
}
{
	Serializer.prototype.deserialize = function(stringToDeserialize)
	{
		var nodeRoot = JSON.parse(stringToDeserialize);
		nodeRoot.__proto__ = SerializerNode.prototype;
		nodeRoot.prototypesAssign();
		var returnValue = nodeRoot.unwrap([]);

		return returnValue;
	}

	Serializer.prototype.serialize = function(objectToSerialize)
	{
		var nodeRoot = new SerializerNode(objectToSerialize);

		nodeRoot.wrap([], []);

		var nodeRootSerialized = JSON.stringify
		(
			nodeRoot, 
			null, // ? 
			4 // pretty-print indent size
		);

		return nodeRootSerialized;
	}
}

function SerializerNode(objectWrapped)
{
	this.objectWrappedTypeName = null;
	this.id = null;
	this.isReference = null;

	this.objectWrapped = objectWrapped;
}
{
	SerializerNode.prototype.wrap = function
	(
		objectsAlreadyWrapped, objectIndexToNodeLookup
	)
	{
		if (this.objectWrapped != null)
		{			
			var typeName = this.objectWrapped.constructor.name;

			var objectIndexExisting = 
				objectsAlreadyWrapped.indexOf(this.objectWrapped);
				
			if (objectIndexExisting >= 0)
			{
				var nodeForObjectExisting = objectIndexToNodeLookup[objectIndexExisting];
				this.id = nodeForObjectExisting.id;
				this.isReference = true;
				this.objectWrapped = null;
			}
			else
			{
				this.isReference = false;
				var objectIndex = objectsAlreadyWrapped.length;
				this.id = objectIndex;
				objectsAlreadyWrapped.push(this.objectWrapped);
				objectIndexToNodeLookup[objectIndex] = this;

				this.objectWrappedTypeName = typeName;
	
				if (typeName == "Function")
				{
					this.objectWrapped = this.objectWrapped.toString();
				}
				else
				{
					this.children = {};
	
					for (var propertyName in this.objectWrapped)
					{
						if (this.objectWrapped.__proto__[propertyName] == null)
						{
							var propertyValue = this.objectWrapped[propertyName];

							if (propertyValue == null)
							{
								child = null;
							}
							else 
							{			
								var propertyValueTypeName = propertyValue.constructor.name;

								if 
								(
									propertyValueTypeName == "Boolean"
									|| propertyValueTypeName == "Number"
									|| propertyValueTypeName == "String"
								)
								{
									child = propertyValue;
								}
								else
								{
									child = new SerializerNode
									(
										propertyValue
									);
								}

							}

							this.children[propertyName] = child;
						}
					}

					delete this.objectWrapped;
	
					for (var childName in this.children)
					{
						var child = this.children[childName];
						if (child != null)
						{
							var childTypeName = child.constructor.name;
							if (childTypeName == "SerializerNode")
							{
								child.wrap
								(
									objectsAlreadyWrapped,
									objectIndexToNodeLookup
								);
							}
						}
					}
				}
			}

		} // end if objectWrapped != null

		return this;		

	} // end method

	SerializerNode.prototype.prototypesAssign = function()
	{
		if (this.children != null)
		{
			for (var childName in this.children)
			{
				var child = this.children[childName];
				if (child != null)
				{
					var childTypeName = child.constructor.name;
					if (childTypeName == "Object")
					{
						child.__proto__ = SerializerNode.prototype;
						child.prototypesAssign();
					}
				}
			}
		}
	}

	SerializerNode.prototype.unwrap = function(nodesAlreadyProcessed)
	{
		if (this.isReference == true)
		{
			var nodeExisting = nodesAlreadyProcessed[this.id];
			this.objectWrapped = nodeExisting.objectWrapped;
		}
		else
		{
			nodesAlreadyProcessed[this.id] = this;
			var typeName = this.objectWrappedTypeName;
			if (typeName == null)
			{
				// Value is null.  Do nothing.
			}
			else if (typeName == "Array")
			{
				this.objectWrapped = [];
			}
			else if (typeName == "Function")
			{
				this.objectWrapped = eval("(" + this.objectWrapped + ")");
			}
			else if 
			(
				typeName == "Boolean" 
				|| typeName == "Number" 
				|| typeName == "String"
			)
			{
				// Primitive types. Do nothing.
			}
			else
			{
				this.objectWrapped = {};
				var objectWrappedType = eval("(" + typeName + ")");
				this.objectWrapped.__proto__ = objectWrappedType.prototype;
			}

	
			if (this.children != null)
			{
				for (var childName in this.children)
				{
					var child = this.children[childName];
			
					if (child != null)
					{
						if (child.constructor.name == "SerializerNode")
						{
							child = child.unwrap
							(
								nodesAlreadyProcessed
							);
						}
					}

					this.objectWrapped[childName] = child;
				}
			}

		}

		return this.objectWrapped;
	}


}

// run

testSerializer();

</script>
</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s