From 3befe3f3d3905daba1d82b76f806dc84a48f9877 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raphael=20R=C3=B6sch?= Date: Mon, 5 Aug 2024 12:52:37 +0200 Subject: [PATCH] AVRO-2032: Add support for NaN, Infinity and -Infinity in JsonDecoder JsonEncoder uses special string values to represent NaN, Infinity and -Infinity values for float and double values, but JsonDecoder does not accept these string values. This change adds support for these special values to JsonDecoder. --- .../java/org/apache/avro/io/JsonDecoder.java | 44 +++++++++++++++++++ .../org/apache/avro/io/TestJsonDecoder.java | 22 ++++++++++ 2 files changed, 66 insertions(+) diff --git a/lang/java/avro/src/main/java/org/apache/avro/io/JsonDecoder.java b/lang/java/avro/src/main/java/org/apache/avro/io/JsonDecoder.java index 2ad496a5b87..a546c6c3c06 100644 --- a/lang/java/avro/src/main/java/org/apache/avro/io/JsonDecoder.java +++ b/lang/java/avro/src/main/java/org/apache/avro/io/JsonDecoder.java @@ -199,6 +199,19 @@ public float readFloat() throws IOException { float result = in.getFloatValue(); in.nextToken(); return result; + } else if (in.getCurrentToken() == JsonToken.VALUE_STRING) { + String stringValue = in.getText(); + in.nextToken(); + if (isNaNString(stringValue)) { + return Float.NaN; + } + if (isNegativeInfinityString(stringValue)) { + return Float.NEGATIVE_INFINITY; + } + if (isPositiveInfinityString(stringValue)) { + return Float.POSITIVE_INFINITY; + } + throw error("float"); } else { throw error("float"); } @@ -211,11 +224,42 @@ public double readDouble() throws IOException { double result = in.getDoubleValue(); in.nextToken(); return result; + } else if (in.getCurrentToken() == JsonToken.VALUE_STRING) { + String stringValue = in.getText(); + in.nextToken(); + if (isNaNString(stringValue)) { + return Double.NaN; + } + if (isNegativeInfinityString(stringValue)) { + return Double.NEGATIVE_INFINITY; + } + if (isPositiveInfinityString(stringValue)) { + return Double.POSITIVE_INFINITY; + } + throw error("double"); } else { throw error("double"); } } + // check whether the given string represents an IEEE 754 'NaN' string value as + // serialized by Jackson + private static boolean isNaNString(String value) { + return "NaN".equals(value); + } + + // check whether the given string represents an IEEE 754 'Infinity' string value + // as serialized by Jackson + private static boolean isPositiveInfinityString(String value) { + return "Infinity".equals(value) || "INF".equals(value); + } + + // check whether the given string represents an IEEE 754 '-Infinity' string + // value as serialized by Jackson + private static boolean isNegativeInfinityString(String value) { + return "-Infinity".equals(value) || "-INF".equals(value); + } + @Override public Utf8 readString(Utf8 old) throws IOException { return new Utf8(readString()); diff --git a/lang/java/avro/src/test/java/org/apache/avro/io/TestJsonDecoder.java b/lang/java/avro/src/test/java/org/apache/avro/io/TestJsonDecoder.java index 1f44344e3aa..03b79f8a5f3 100644 --- a/lang/java/avro/src/test/java/org/apache/avro/io/TestJsonDecoder.java +++ b/lang/java/avro/src/test/java/org/apache/avro/io/TestJsonDecoder.java @@ -118,4 +118,26 @@ void testIntWithError() throws IOException { JsonDecoder decoder = DecoderFactory.get().jsonDecoder(schema, record); Assertions.assertThrows(AvroTypeException.class, () -> reader.read(null, decoder)); } + + @Test + void testIeee754SpecialCases() throws IOException { + String def = "{\"type\":\"record\",\"name\":\"X\",\"fields\": [" + "{\"type\":\"float\",\"name\":\"nanFloat\"}," + + "{\"type\":\"float\",\"name\":\"infinityFloat\"}," + + "{\"type\":\"float\",\"name\":\"negativeInfinityFloat\"}," + "{\"type\":\"double\",\"name\":\"nanDouble\"}," + + "{\"type\":\"double\",\"name\":\"infinityDouble\"}," + + "{\"type\":\"double\",\"name\":\"negativeInfinityDouble\"}" + "]}"; + Schema schema = new Schema.Parser().parse(def); + DatumReader reader = new GenericDatumReader<>(schema); + + String record = "{\"nanFloat\":\"NaN\", \"infinityFloat\":\"Infinity\", \"negativeInfinityFloat\":\"-Infinity\", " + + "\"nanDouble\":\"NaN\", \"infinityDouble\":\"Infinity\", \"negativeInfinityDouble\":\"-Infinity\"}"; + Decoder decoder = DecoderFactory.get().jsonDecoder(schema, record); + GenericRecord r = reader.read(null, decoder); + assertEquals(Float.NaN, r.get("nanFloat")); + assertEquals(Float.POSITIVE_INFINITY, r.get("infinityFloat")); + assertEquals(Float.NEGATIVE_INFINITY, r.get("negativeInfinityFloat")); + assertEquals(Double.NaN, r.get("nanDouble")); + assertEquals(Double.POSITIVE_INFINITY, r.get("infinityDouble")); + assertEquals(Double.NEGATIVE_INFINITY, r.get("negativeInfinityDouble")); + } }