Wednesday, 17 July 2013

Another day, another ColdFusion JSON bug (and a Railo bug too, just not with JSON)

G'day:
Well at least this time it wasn't something that caught me out. This one wasted someone else's time.

I keep an eye on @CFBugNotifier on Twitter (yes: that was a gratuitous plug for my own work), as it's good to know where it's safe to tread in ColdFusion, and where it's better to stay well clear of. I think we've established that ColdFusion's JSON handling is one of those areas to - if not stay clear of - tread with caution. Today a new JSON bug presented itself, in the form of 3596207. This is another example of ColdFusion being unable to correctly identify its own data types. The gist of it is that the string "1." gets kinda treated like a numeric by ColdFusion, but not really, and in the process CF serialises it incorrectly. There's a repro on the ticket, but I looked into it further. And I'm writing this up as there's some odd behaviour in ColdFusion (as per above), but some unstable behaviour with Railo as well, this time (not relating to JSON though).

Here's my test code:

s = "1.";

try{
    param name="s" type="numeric";
    paramAsNumeric = true;
}
catch (any e){
    paramAsNumeric = {message=e.message,detail=e.detail};
}

try{
    param name="s" type="float";
    paramAsFloat = true;
}
catch (any e){
    paramAsFloat = {message=e.message,detail=e.detail};
}    
try{
    param name="s" type="integer";
    paramAsInteger = true;
}
catch (any e){
    paramAsInteger = {message=e.message,detail=e.detail};
}

isNumeric = isNumeric(s);
isValidFloat = isValid("float", s);
isValidInteger = isValid("integer", s);

original = {key=s};
serialised = serializeJson(original);
isValidJson = isJson(serialised);

deserialised = deserializeJson(serialised);

writeDump([
    {s=s},
    {paramAsNumeric=paramAsNumeric},
    {paramAsFloat=paramAsFloat},
    {paramAsInteger=paramAsInteger},
    {isNumeric=isNumeric},
    {isValidFloat=isValidFloat},
    {isValidInteger=isValidInteger},
    {original=original},
    {serialised=serialised},
    {isValidJson=isValidJson},
    {deserialised=deserialised}
]);

And here's the output (it's the same on CF 9.02 and 10.0.11):

array
1
struct
S1.
2
struct
PARAMASNUMERICtrue
3
struct
PARAMASFLOATtrue
4
struct
PARAMASINTEGER
struct
DETAILThe value specified, 1., must be a valid integer.
MESSAGEInvalid parameter type.
5
struct
ISNUMERICYES
6
struct
ISVALIDFLOATYES
7
struct
ISVALIDINTEGERNO
8
struct
ORIGINAL
struct
KEY1.
9
struct
SERIALISED{"KEY":1.}
10
struct
ISVALIDJSONYES
11
struct
DESERIALISED
struct
KEY1

I've highlighted a few things:
  • I really don't think CF should be treating "1." as a numeric value of any stripe. Numbers don't end with decimal points. Obviously they can have decimal points, but if they do, they need at least one digit after it. That said, it does think this expression is legit:
    f = 1.;
    And rooting around a bit, actually Java, Javascript, PHP and Python all accept this sort of thing. Ruby doesn't. Hmmm. Well I don't agree with the majority here, but I concede it's an unpopular position to take.
  • Next, ColdFusion takes the string "1." and turns it into the "number" 1., and then back into a string 1. All three of those are different things. Only the first one is right, in the given context.
  • ColdFusion maintains that JSON is valid. It's not. JSON (or RFC-4627) does not accept that numbers can end in a decimal point. You need to follow the rules, Adobe.
I s'pose I better let ColdFusion off the whole "numbers ending with decimal points" thing, as there's precedent set for it. But not in JSON, and it should deal with "1." reliably when converting it to JSON (and back). This is another case of CF "not getting" that just cos it's very loosely typed, other systems are not, so it needs to respect the notion of types when it's exporting data.



Now... Railo has a different issue here. Here's the dump from Railo (4.1.0.011):


Array
1
Struct
S
string1.
2
Struct
PARAMASNUMERIC
Struct
DETAIL
stringJava type of the object is java.lang.String
MESSAGE
stringCan't cast String [1.] to a value of type [numeric]
3
Struct
PARAMASFLOAT
Struct
DETAIL
stringJava type of the object is java.lang.String
MESSAGE
stringCan't cast String [1.] to a value of type [float]
4
Struct
PARAMASINTEGER
Struct
DETAIL
stringJava type of the object is java.lang.String
MESSAGE
stringCan't cast String [1.] to a value of type [integer]
5
Struct
ISNUMERIC
booleanfalse
6
Struct
ISVALIDFLOAT
booleanfalse
7
Struct
ISVALIDINTEGER
booleantrue
8
Struct
ORIGINAL
Struct
KEY
string1.
9
Struct
SERIALISED
string{"KEY":"1."}
10
Struct
ISVALIDJSON
booleantrue
11
Struct
DESERIALISED
Struct
KEY
string1.

I'm pleased the param "expressions" failed on Railo... although perhaps it should conform with ColdFusion here? I dunno.

However... spot the obvious mistake. param doesn't accept "1." as any of a numeric, float or integer... but isValid() thinks "1." is a valid integer. Whilst not being a valid float. Or even numeric. Oops. It's gotta get its messaging straight here!

Railo also reports isJson() as true here, but in Railo's case that's correct. It actually creates the JSON properly: it preserves the "1." value all the way through to JSON and back again.

Right. That was too much to write down about "numbers" ending in decimal points, really, wasn't it? What do you think about 1. being a number? Yes? No? Simply don't care, Adam? ;-)

Cheers.

--
Adam