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

217 views Asked by At

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?
2

There are 2 answers

4
BallpointBen On BEST ANSWER

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}
6
Masklinn On

It is unsupported.

The maintainer's answer is not entirely clear as I don't think serde_json exposes deserializer hooks through which you could add a fallback, and I would think the usual solution (deserialize_with) would be too late for that.

So you might want to ask for precisions, but at first glance it looks like you'd have to maintain a customised vendoring / fork of serde_json to support non-standard extensions.