Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Consuming Sparse, Unpredictable "omitempty" Golang JSON Payloads Using Null Coalescing In Lucee CFML 5.3.7.48

By Ben Nadel on

At InVision, I'm working on yet another "remonolithification" project, this time merging a Go service into my ColdFusion monolith. As part of this subsumption, I have to write CFML code that consumes the JSON (JavaScript Object Notation) payload being returned from a different Go service. I have basically no Go experience; so, this endeavor has been comically challenging given the simplicity of the service that I'm tearing down. It turns out, in Go, you can use an omitempty flag in your deserialization process to make your return payloads wildly unpredictable. To translate the sparse, unpredictable, and potentially missing data into a predictable ColdFusion format, I'm using the null coalescing operator (aka, the "Elivs" Operator) in Lucee CFML 5.3.7.48.

First, a huge shout-out to Bryan Stanley, who has taken time out of his very busy schedule to walk me through some Go code; and to hear me rant excessively about the aspects of Go that make no sense to me as a ColdFusion developer. Such as why on earth you would want to make your JSON payloads harder for other services to consume?! You are a true hero!

At first, I thought subsuming this Go service would require little more than a few simple HTTP calls followed by a few simple deserializeJson() calls. But, when I first tried to implement this naive approach, my ColdFusion code started blowing up due to missing Struct keys. When I dug into the Go code, I noticed that the vast majority of keys have an omitempty notation attached to them. They all looked something like this (pseudo-code):

type PrototypeType struct {
	ID                  string      `json:"id,omitempty"`
	Name                string      `json:"name"`
	UserID              int         `json:"userId"`
	CompanyID           int         `json:"companyId,omitempty"`
	Archived            bool        `json:"archived,omitempty"`
	Background          *ColorType  `json:"background,omitempty"`
	Device              *DeviceType `json:"device,omitempty"`
}

type ColorType struct {
	R int     `json:"r"`
	G int     `json:"g"`
	B int     `json:"b"`
}

type DeviceType struct {
	Appearance          *AppearanceType  `json:"appearance,omitempty"`
	ID                  string           `json:"id,omitempty"`
	Height              int              `json:"height,omitempty"`
	Width               int              `json:"width,omitempty"`
	Scaling             string           `json:"scaling,omitempty"`
	ScaleToFitWindow    bool             `json:"scaleToFitWindow,omitempty"`
}

type AppearanceType struct {
	Type                string        `json:"type,omitempty"`
	IsRotated           bool          `json:"isRotated,omitempty"`
	Skin                string        `json:"skin,omitempty"`
}

Notice that most of the json annotations on these complex objects have omitempty. According to the Go documentation on JSON:

The "omitempty" option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.

So basically what this means is that when I call this Go service and it returns JSON, the shape of that JSON data is going to be completely different depending on how many "empty" values happen to be in the serialized structure.

It seems unfortunate that an API would have an unpredictable data model; but, thankfully, I can use the null coalescing operator in ColdFusion, ?:, to cast undefined values into the appropriate "empty" values that Go stripped out:

cfml_value = ( unpredictable_go_value ?: empty_value )

It makes my ColdFusion translation layer rather verbose; but, at least it's mostly straightforward and leaves me with a data structure that I can depend on. Here's an example of how I'm handling this in my Lucee CFML code:

<cfscript>

	// This represents the sparse JSON payload that might come back from a Go service.
	// The "omitempty" flags on the serialization definitions mean that any "empty" value
	// will be completely removed from the resultant JSON payload.
	goModel = {
		name: "My Prototype",
		userId: 1234,
		companyId: 5678,
		background: {
			r: 123,
			g: 0,
			b: 227
		},
		device: {
			id: "d7-landscape",
			appearance: {
				type: "mobile"
			}
		}
	};

	dump(
		label = "Golang Model",
		var = goModel
	);
	dump(
		label = "Lucee CFML Model",
		var = translatePrototypeModel( goModel )
	);

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I translate the sparse, unpredictable, potentially missing Go model for
	* AppearanceType into a predictable ColdFusion model.
	* 
	* type AppearanceType struct {
	*     Type                string        `json:"type,omitempty"`
	*     IsRotated           bool          `json:"isRotated,omitempty"`
	*     Skin                string        `json:"skin,omitempty"`
	* }
	* 
	* @appearanceModel I am the Go model being translated.
	*/
	public struct function translateAppearanceModel( struct appearanceModel ) {

		return({
			type: ( appearanceModel.type ?: "" ),
			isRotated: ( appearanceModel.isRotated ?: false ),
			skin: ( appearanceModel.skin ?: "" )
		});

	}


	/**
	* I translate the potentially missing Go model for ColorType into a predictable
	* ColdFusion model (a 6-digit HEX string).
	* 
	* type ColorType struct {
	*     R int     `json:"r"`
	*     G int     `json:"g"`
	*     B int     `json:"b"`
	* }
	* 
	* @colorModel I am the Go model being translated.
	*/
	public string function translateColorModel( struct colorModel ) {

		if ( isNull( colorModel ) ) {

			return( "" );

		}

		var r = formatBaseN( colorModel.r, 16 ).lcase();
		var g = formatBaseN( colorModel.g, 16 ).lcase();
		var b = formatBaseN( colorModel.b, 16 ).lcase();

		return(
			right( "0#r#", 2 ) &
			right( "0#g#", 2 ) &
			right( "0#b#", 2 )
		);

	}


	/**
	* I translate the sparse, unpredictable, potentially missing Go model for
	* DeviceType into a predictable ColdFusion model.
	* 
	* type DeviceType struct {
	*     Appearance          *AppearanceType  `json:"appearance,omitempty"`
	*     ID                  string           `json:"id,omitempty"`
	*     Height              int              `json:"height,omitempty"`
	*     Width               int              `json:"width,omitempty"`
	*     Scaling             string           `json:"scaling,omitempty"`
	*     ScaleToFitWindow    bool             `json:"scaleToFitWindow,omitempty"`
	* }
	* 
	* @deviceModel I am the Go model being translated.
	*/
	public struct function translateDeviceModel( struct deviceModel ) {

		return({
			appearance: translateAppearanceModel( deviceModel.appearance ?: nullValue() ),
			id: ( deviceModel.id ?: "" ),
			height: ( deviceModel.height ?: 0 ),
			width: ( deviceModel.width ?: 0 ),
			scaling: ( deviceModel.scaling ?: "" ),
			scaleToFitWindow: ( deviceModel.scaleToFitWindow ?: false )
		});

	}


	/**
	* I translate the sparse, unpredictable Go model for PrototypeType into a predictable
	* ColdFusion model.
	* 
	* type PrototypeType struct {
	*     ID                  string      `json:"id,omitempty"`
	*     Name                string      `json:"name"`
	*     UserID              int         `json:"userId"`
	*     CompanyID           int         `json:"companyId,omitempty"
	*     Archived            bool        `json:"archived,omitempty"`
	*     Background          *ColorType  `json:"background,omitempty"`
	*     Device              *DeviceType `json:"device,omitempty"`
	* }
	* 
	* @prototypeModel I am the Go model being translated.
	*/
	public struct function translatePrototypeModel( required struct prototypeModel ) {

		return({
			id: ( prototypeModel.id ?: "" ),
			name: prototypeModel.name,
			userID: prototypeModel.userId,
			companyID: ( prototypeModel.companyId ?: 0 ),
			isArchived: ( prototypeModel.archived ?: false ),
			backgroundColor: translateColorModel( prototypeModel.background ?: nullValue() ),
			device: translateDeviceModel( prototypeModel.device ?: nullValue() )
		});

	}

</cfscript>

As you can see, in order to manage the translation of data, I have to recurse down through the data structures, mapping each Go value onto the appropriate ColdFusion value. For values that are defined, they come through as is; but, if the value is missing, I have to default it to the "empty" value that Go removed. Like I said above, it's verbose. But, when we run this ColdFusion code we get the following output:

Go data structure translated into a CFML data structure using the null coalescing operator in Lucee CFML.

As I stated above, I don't really have any Go experience. As such, I can't speak to why a Go API would want to return an unpredictable JSON structure. For me, that just seems to make the response data harder to consume. It feels a little bit like a premature optimization of data-size over the network (at the cost of developer ergonomics). That said, at least I can easily - if not pedantically - marshal those return payloads into something I can safely consume in Lucee CFML 5.3.7.48.



Reader Comments

I love the elvis operator. It's so useful. Depending on the situation I'll use it instead of cfparam. As in:

form.name = form.name ?: "";
form.zip = form.zip ?:"";

Reply to this Comment

@Hugh,

100% It's a great operator. Though, I also do use the <cfparam> tag for a lot of the form-value defaulting. That's what I love about ColdFusion - so many powerful options!

Reply to this Comment

@All,

Over the weekend, I realized that I accidentally discovered that my mental model for the safe navigation operator was incomplete. I always thought it tested the left operand only. However, it appears to test both the left and right operands when short-circuiting:

www.bennadel.com/blog/4017-the-safe-navigation-operator-checks-both-left-and-right-operands-in-coldfusion.htm

What this means is that most of my use of the Elvis operator in this post can actually be replaced with safe navigation operators. So, for example, this:

translateAppearanceModel( deviceModel.appearance ?: nullValue() )

... can actually become just this:

translateAppearanceModel( deviceModel?.appearance )

... because this expression will return null if either deviceModel or deviceModel.appearance is undefined.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.