Skip to main content
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Susan Brun
Ben Nadel at cf.Objective() 2017 (Washington, D.C.) with: Susan Brun

Encapsulating Serialization Logic In ColdFusion

By
Published in

Most of the time, when I need to serialize data in ColdFusion, I just reach for the built-in serializeJson() function. And, in most cases, this is a perfectly fine approach. But sometimes, I need to massage or manipulate the data as part of the serialization / deserialization workflow. If this is part of my database access layer (DAL), I might put that logic right in my data gateway; but, if I'm just dealing with flat text files, I've been encapsulating this logic inside its own abstraction.

This came up for me over the weekend while working on the companion app for my Feature Flags book. My companion app doesn't use a database; and just saves feature flags configuration data to a .json file. Part of this data includes date/time values. Date/time values serialize fine into JSON strings; but, they deserialize back into ColdFusion strings. And, I need them to be parsed back into native date/time values.

To make sure that I always end up with the proper data types, I've moved the serializeJson() and deserializeJson() calls into their own ColdFusion component that is entity-specific. This component then exposes its own serialize and deserialize methods.

To explore this concept, imagine that I have a collection of users. And, each user has a createdAt date/time property. When I serialize this collection, I wanted it to be persisted as a Newline-Delimited JSON (ND-JSON) payload. And, when it is parsed, I wanted to make sure that the createdAt property is a date/time value.

I've put the logic for this serialization / deserialization in a component, UserSerializer.cfc":

component
	output = false
	hint = "I encapsulate the serialization / deserialization logic for the users."
	{

	/**
	* I serialize the given users collection into a persistable string. The string
	* represents a newline-delimited JSON (ND-JSON) payload.
	*/
	public string function serializeUsers( required array users ) {

		var results = users
			.map(
				( user ) => {

					// Since we need to mutate some of the data in preparation for
					// serialization we must create a duplicate of it. Since all of this
					// data is passed by-reference, we need to make sure we don't corrupt
					// a data structure that is still in use elsewhere in the request.
					var data = duplicate( user );

					// Native date/times don't serialize well. Let's convert them to an
					// ISO format for easier parsing later on.
					data.createdAt = data.createdAt.dateTimeFormat( "iso" );

					return serializeJson( data );

				}
			)
			.toList( chr( 10 ) )
		;

		return results;

	}


	/**
	* I deserialize the given users string back into a users collection.
	*/
	public array function deserializeUsers( required string input ) {

		var results = input
			.reMatch( "[^\n]+" )
			.map(
				( dataInput ) => {

					var user = deserializeJson( dataInput );

					// When we serialized the user, we flattened the dates as ISO strings.
					// Now, when recreating the user, we have to parse the ISO strings
					// back into date/time values.
					user.createdAt = parseDateTime( user.createdAt );

					return user;

				}
			)
		;

		return results;

	}

}

There are two important aspects to this ColdFusion component. First, it calls duplicate() on the passed-in user data. It does this because it needs to massage the data prior to persistence; and, we can't risk the chance that we're mutating data that is still being referenced by the calling context (and which may be consumed later on in the same request). By calling duplicate(), we can be sure that we're working with a deeply-cloned, isolated copy of the data to be serialized.

Second, when serializing the user data we're explicitly overwriting the createdAt property to be an ISO-formatted string. Then, when deserializing the user data, we're explicitly casting that ISO-formatted string back into a native date/time value by calling parseDateTime(). This way, we have native date/time values on the way in and on the way out.

To test this, let's create a collection of users, run it through the serialization / deserialization process, and then compare the result to the original input:

<cfscript>

	// This component encapsulates any of the logic we need to serialize / deserialize
	// the user data. This includes logic to massage data that doesn't otherwise serialize
	// so nicely.
	userSerializer = new UserSerializer();

	users = [
		[ id: 1, name: "Steve", createdAt: now().add( "d", -7 ) ],
		[ id: 2, name: "Johannah", createdAt: now().add( "d", -4 ) ],
		[ id: 3, name: "Laura", createdAt: now().add( "d", -2 ) ]
	];

	// -- SERIALIZE TO STRING (ND-JSON) --

	filename = "./data.ndjson";
	fileWrite( filename, userSerializer.serializeUsers( users ) );

	// -- DESERIALIZE BACK INTO COLDFUSION ARRAY --

	usersPrime = userSerializer.deserializeUsers( fileRead( filename, "utf-8" ) );

</cfscript>
<cfoutput>

	<div>
		<h2>
			Pre-Serialization
		</h2>
		<cfdump
			var="#users#"
			label="Original Data"
		/>
	</div>
	<div>
		<h2>
			Post-Serialization
		</h2>
		<cfdump
			var="#usersPrime#"
			label="Reloaded Data"
		/>
	</div>

</cfoutput>

When we run this ColdFusion code, we persist an .ndjson file with our serialized user data (note the ISO strings for the createdAt property):

{"id":1,"name":"Steve","createdAt":"2024-06-24T08:44:34-04:00"}
{"id":2,"name":"Johannah","createdAt":"2024-06-27T08:44:34-04:00"}
{"id":3,"name":"Laura","createdAt":"2024-06-29T08:44:34-04:00"}

And, we end up with a ColdFusion array-of-structs which exactly mirrors the original input:

Two user collections, one before serialization and one after deserialization, showing the exact same data in ColdFusion.

As you can see, both the pre-serialization and the post-serialization data structures are exactly the same. Including native date/time stamps for the createdAt properties.

I don't believe this level of encapsulation is always necessary. But, when the data needs to be massaged / manipulated as part of the serialization workflow, having an additional layer of encapsulation can keep things simple.

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel