Encapsulating Serialization Logic In ColdFusion
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:
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 →