Fun With IsDefined() vs KeyExists()
I write a lot of apps that interact with external APIs. Often times the data returned by 3rd parties is a deeply nested structure of data - and occasionally the 3rd party isn't reliable enough to send consistent response structures so I need to check for the existence of structure keys to avoid errors (frustrating, I know).
Here's an example of what several response objects might look like from the same API
{
// Response sample 1:
apiResponse: {
data: {
errors: [ "surname is invalid" ]
}
}
// Response sample 2:
apiResponse: {
error: "The system is currently offline"
}
// Response sample 3:
apiResponse: {
data: {
name: [ "john smith" ]
}
}
}
When I digest the data, I check the `apiResponse` for the existence of keys and sub keys before processing. However, it can be pretty tedious from a coding perspective to write out expressions that check for the existence of every single sub-key like this:
if (
arguments.apiResponse.keyExists( "data" ) &&
arguments.apiResponse.data.keyExists( "errors" )
) {
// ... do something
}
I wanted to simplify the code, and unfortunately `structKeyExists()` does not support nested keys.
My first instinct was to use the 'isDefined()` function to check for the existence of the full struct key path. However, the `isDefined()` function has been mostly vilified by the CFML community for performance/security reasons (related discussion related StackOverflow)
Here's the same `if` statement from above using `isDefined()` instead:
if ( isDefined( "arguments.apiResponse.data.errors" ) ) {
// ... do something
}
Much cleaner, right? Howver, what about the performance implications? `isDefined()` by design will check various scopes to see if the variable exists, which can be slow.
I wrote a small UDF that I theorized would be more efficient than `isDefined()` and could allow me to dynamically check a struct for the existence of a key. Here's what I came up with:
boolean function structHasKey( required struct struct, required string key ) {
var keyArray = listToArray( arguments.key, "." );
var subStruct = arguments.struct;
for ( var item in keyArray ) {
if ( !subStruct.keyExists( item ) ) {
return false;
}
subStruct = subStruct[ item ];
}
return true;
}
With this new UDF, we can make the same check as above like this:
if ( structHasKey( arguments.apiResponse, "data.errors" ) ) {
// ... do something
}
I wrote a simple benchmark to see how this UDF stacks up against `isDefined()` and the traditional approach of using `structKeyExists()`. Now, I realize TryCF isn't the most scientifically appropriate benchmarking tool, but it's interesting to see how the various CFML engines (Adobe/Lucee) handle the different approaches. I also believe `isDefined()` must perform differently based on the number of variables present in the URL/FORM (and other) scopes that it checks.
After running the TryCF gist 20-30 times, the fastest method was the old-school `structKeyExists()`. Both the UDF method and `isDefined()` traded between 2nd and 3rd place quite often - especially depending on the CF engine so I wasn't able to make a final consensus. If I were a betting man, i would have thought my UDF was going to beat `isDefined()` every time, but the overhead of executing the UDF must outweigh any benefit of the approach.
If anyone has any tips on improving the UDF, or my test setup, let me know as I'd love to play with this concept more.
