Search code examples
jsonrustieee-754serdeserde-json

Is there a way to make serde_json handle NaN, Inf and -Inf properly/improperly (IEEE 754 Specials)?


The official JSON spec doesn't support IEEE 754, and instead has its own convention of null (not "null") or normal numbers.

In many languages and use cases people ignore this and deviate from the spec to support IEEE754 floats. For example in python

>>> json.dumps(dict(a = np.inf, b = -np.inf, c = np.nan), allow_nan=True)
'{"a": Infinity, "b": -Infinity, "c": NaN}'

The allow_nan defaults to True in this case.

Likewise in C# we can set the number handling to AllowNamedFloatingPointLiterals to get the same behaviour

https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonnumberhandling?view=net-8.0

So, how can we get rust/serde_json to do the same thing - is there a flag somewhere in serde_json to do this, and if not, what would be the simplest way to add this feature? (I mean achieve this feature as a user, not by updating the serde_json source or forking it or anything).

Edit: Following some of the comments, suppose we agree that JSON is at fault, is there a format that could be used in place of JSON, that fully support floats.

Alternatively, how could one implement a JSON valid alternative like using "Infinity" as a string. As far as I know this would affect all other serialisations, so if you serialise the struct to BSON, CBOR, msgpack etc.

Edit again

So my own research has thrown up a couple of possibles:

  • JSON5 apparently expands on JSON, while being backward compatible.
  • There might be some magic trickery that could be done with serde untagged enums, but I don't know if this is true. It seems that serde can try a sequence of formats in turn until one succeeds - is there a way to make serde_json FAIL if it tries to serialise a Special Float (which is, strictly, what JSON spec says it should do). If so we could use this as a fall back maybe?

Solution

  • Can serde_json be made to handle these special floats? No, because it adheres strictly to the JSON spec. However, JSON5 supports these funky floats, and serde_json5 faithfully implements JSON5:

    use std::collections::HashMap;
    
    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let map = HashMap::<_, _>::from_iter([
            ("a", 1.0),
            ("b", f64::NAN),
            ("c", f64::INFINITY),
            ("d", f64::NEG_INFINITY),
        ]);
    
        let j = serde_json5::to_string(&map)?;
        let o = serde_json5::from_str::<HashMap<String, f64>>(&j)?;
    
        println!("JSON5: {}", j);
        println!("HashMap: {:?}", o);
    
        Ok(())
    }
    

    Output:

    JSON5: {"a":1,"b":NaN,"d":-Infinity,"c":Infinity}
    HashMap: {"a": 1.0, "d": -inf, "c": inf, "b": NaN}