So, reporting back as promised…
In the end I went with a modified version of the code @andreas suggested. See below. Thank you!
myJsonConverter = new MyJsonConverter();
j1 = MyJsonConverter.serializeJsonWithLowerKeyNames( myNestedStruct );
j2 = MyJsonConverter.serializeJsonWithLowerKeyNames( myArrayOfNestedStructs );
In the CF10 legacy code, I am using Taffy and the serialization is done in just one place, so having this there simplifies the process as I can catch all requests for JSON in the one place. That was preferable to manually changing all the structs at origin because there were many and there was a good chance of missing some, given the way the data objects were being constructed (and it is just me - no team )
In terms of overhead, unless the object being serialized is very large, the speed is acceptable:
Average test results comparing the native Lucee SerializeJSON and the convert-to-lowercase function:
17 small records: 985 bytes of json:
native: 85 ms
lowercased: 102 ms
13 large, nested records, 34 Kb of json
native: 5268 ms
lowercased: 5340 ms
3,450 large nested records, 1.4 Mb of json Yes - big!
native: 6630 ms
lowercased: 13320 ms
I also made a language proposal here: Topic 10636
/**
CFC to call the Lucee serializeJSON() after converting all the keys
in the passed in data object to lowercase.
eg:
myJsonConverter = new MyJsonConverter();
j1 = MyJsonConverter.serializeJsonWithLowerKeyNames( myNestedStruct );
j2 = MyJsonConverter.serializeJsonWithLowerKeyNames( myArrayOfNestedStructs );
See: https://lucee.daemonite.io/t/serializejson-with-lowercase-keys-and-override-jsonconverter-java/10619
for the rationale and
credit to @andreas for the initial code in that post. :-)
*/
component displayname="MyJsonConverter.cfc" output="false" {
public struct function init(){
return this;
}
/**
PUBLIC
Uses the Lucee native serializeJSON() to serialize the object to JSON,
AFTER converting all keys to lowercase.
*/
public string function serializeJsonWithLowerKeyNames ( any dataobject required ){
if (isArray( dataobject )) {
// We might start with an array at the top level
return "[#serializeJSON( lowerStructKeyNames( arguments.dataobject[1]) )#]";
} else {
// Or a struct, or simple value
return serializeJSON( lowerStructKeyNames( arguments.dataobject ) );
}
}
/**
PRIVATE
If the dataobject is a struct or array,
for all keys in that object convert them to lower case
and return a new object with those lower case keys.
If the dataobject is a simple value, just return it unchanged.
*/
private any function lowerStructKeyNames ( any dataobject required ){
if (isSimpleValue(dataobject)) {
// Just return the simple value unchanged
local.result = dataobject;
} else {
local.result=[:];
arguments.dataobject.each( function( key, value ) {
if( isStruct( arguments.value )){
// Structs recurse ...
result.append( { "#lcase(arguments.key)#": lowerStructKeyNames ( arguments.value ) } );
} else if (isArray( arguments.value )){
// Array, iterate and recurse ...
var arr = [];
arguments.value.each( function (item) {
arr.append( lowerStructKeyNames ( item ) );
});
result.append( { "#lcase(arguments.key)#": #arr# } );
} else {
// Everything else, just transform
result.append( { "#lcase(arguments.key)#": arguments.value } );
}
});
}
return local.result;
}
}