Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Joshua Ford
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Joshua Ford

Struct Keys That Contain Dots "Just Work" In Adobe ColdFusion

By
Published in Comments (1)

Over the weekend, one of the CFML features that I explored in my Adobe ColdFusion 2025 hackathon entry was the use of a .properties file. A .properties file is just a simple list of key-value pairs that ColdFusion will read into a struct. In my exploration, I had defined one of the keys as, team.name. And, I was surprised that addressing the key without quotes "just worked" in my CFML. This does not work in Lucee CFML; but, apparently, this has worked in Adobe ColdFusion since... forever?

This is not the first time that I've seen this kind of behavior in Adobe ColdFusion. From within ColdFusion custom tags, Adobe has always allowed the caller scope to seamlessly access contextual data using a dot-delimited key. Presumably, this is how native custom tags like CFSaveContent allow for "key paths" to work in the variable attribute. Example:

<cfsavecontent variable="local.buffer">

In this case, the CFSaveContent custom tag is capturing the generated content of the custom tag and storing it into the function-local variable, buffer. If we needed to implement this ourselves in Adobe ColdFusion, we could just do:

caller[ attributes.variable ] = thistag.generatedContent

Which would translate into:

caller[ "local.buffer" ] = thistag.generatedContent

... which would properly set the buffer key within the local scope of the calling context.

Aside: this does not work in Lucee CFML. However, in Lucee CFML, you can use either cfparam or setVariable() to accomplish the same thing in a ColdFusion custom tag.

Now, for years, I assumed that this was something special about the caller scope in a custom tag context. What I didn't realize (or didn't remember) until the hackathon was that this was kind of a generalized feature of Adobe ColdFusion.

To demonstrate, I'm going to use an .ini file (since I wanted to test this in Adobe ColdFusion 2021, which doesn't support .properties):

[first.section]

	key=value
	sub.key=sub.value

[second.section]

	key=value
	sub.key=sub.value

In this .ini file, the first.section and second.section are atomic keys. And, within those sections, the sub.key is also an atomic key. Normally in CFML, we'd have to address these keys using bracket-notation:

[ "first.section" ][ "sub.key" ]

... which would work in all CFML engines. However, in Adobe ColdFusion specifically, we can just use:

first.section.sub.key

To demonstrate, I'll read-in this .ini file and do exactly that:

<cfscript>

	include "./mixins.cfm";

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

	data = getProfileData( expandPath( "./data.ini" ) );
	dump( data );

	// We know this will work in all CFML engines, since this is just normal key look-up.
	echoLine( data[ "first.section" ][ "sub.key" ] );

	try {

		echoLine( data.first.section.sub.key );

	} catch ( any error ) {

		echoLine( "Accessing [data.first.section.sub.key] failed." );
		echoLine( error.message );

	}

</cfscript>

If we run this in both Adobe ColdFusion 2021 and Lucee CFML, we get the following output:

Screenshot of both Adobe ColdFusion and Lucee CFML output.

As you can see, Adobe ColdFusion 2021 had no problem addressing the nested value using keys-with-dots. However, Lucee CFML throws an error.

As a code kata, I wanted to see if could "normalize" this behavior with a user defined function (UDF), structAccess(). The goal here would be to achieve the same behavior in both engines.

<cfscript>

	include "./mixins.cfm";

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

	data = getProfileData( expandPath( "./data.ini" ) );
	dump( data );

	// Normalized access in both CFML engines.
	dump( structAccess( data, "first.section.sub.key" ) );

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

	/**
	* I walk the target sturcture, looking the given path (only works with dots).
	*/
	public any function structAccess(
		required struct target,
		required string objectPath
		) {

		var segments = objectPath.listToArray( "." );
		var path = "";
		var key = "";

		// While we have segments, we're going to build up a complex, dot-delimited key
		// until we find one that matches within the current target.
		while ( segments.isDefined( 1 ) ) {

			// Append next segment to the working path.
			path &= ( "." & segments.shift() );
			key = path.right( -1 );

			if ( target.keyExists( key ) ) {

				target = target[ key ];
				path = key = "";

			}

		}

		// We only want to return a target if we fully consumed the last key.
		if ( ! key.len() ) {

			return target;

		}

	}

</cfscript>

In the structAccess() function, we split the object path on the . character. And then, we keep building up the "next key", one segment at a time, until we find a key that matches within the current target. This allows both Adobe ColdFusion and Lucee CFML to work the same way:

Screenshot of both Adobe ColdFusion and Lucee CFML output.

As you can see, when using structAccess(), both Adobe ColdFusion and Lucee CFML were able to access the nested value with the funky path.

This functionality creates ambiguity; but, I have to say, I kind of like it.

As a final note, there is no getProfileData() in ColdFusion - it only has getProfileSections() and getProfileString(). My getProfileData() usage above is just a user defined function to hide-away the complexity of using a .ini file:

<cfscript>

	/**
	* I load the full INI file contents into the resultant data structure.
	*/
	public struct function getProfileData( required string filepath ) {

		// The profile sections are returned as a struct in which each key is a list of
		// keys available within the given section. We're going to map that back onto a
		// nested structure.
		return getProfileSections( filepath ).map(
			( section, keys ) => {

				var data = [:];

				for ( var key in keys.listToArray() ) {

					data[ key ] = getProfileString( filepath, section, key );

				}

				return data;

			}
		);

	}


	/**
	* I wrap echo output in a div for spacing (in Adobe ColdFusion).
	*/
	public string function echoLine( required string value ) {

		return echo( "<div> #value# </div>" );

	}

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

	// Polyfills for Lucee dumpers.
	if ( ! getFunctionList().keyExists( "echo" ) ) {

		echo = ( value ) => writeOutput( value );
		dump = ( value ) => writeDump( var = value, argumentCollection = arguments );

	}

</cfscript>

Epilogue on BoxLang

Out of curiosity, I wanted to see where BoxLang's CFML engine would come down on the matter. So, I put this into Try BoxLang:

data = {
  "sub.key": "value"
};

dump( data.sub.key );

And it outputs a similar error as Lucee CFML:

The key [sub] was not found in the struct. Valid keys are ([sub.key]).

So, it seems this is dot-key behavior is specific to Adobe ColdFusion.

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

Reader Comments

7 Comments

Hi Ben!

I found that Lucee can do some serious acrobatics with struct keys.

When wrestling with the OpenAPI spec I stumbled into using StructKeyTranslate() and SetVariable() to do some heavy lifting.

I planned to write something on the Lucee Dev site based on the notes I'd made. Ever hear the joke about laughter and plans?

I put my chaotic notes into this Gist. It's a little hard to follow but I think you'll appreciate the 🤪🤔🤓factor!

https://trycf.com/gist/61610b14f7b58e3b5d37f23e096912b5/lucee5?theme=monokai

It works in Lucee 5+.
Cheers!
RayV

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