Use BigDecimal to encode exotic number types.

Previously we'd retain whatever types the caller passed in. This was
potentially problematic for non-immutable numeric types like AtomicInteger.
This commit is contained in:
jwilson
2017-02-01 00:29:36 -05:00
parent 3c23da50cf
commit fdaecb9fb8
3 changed files with 242 additions and 9 deletions

View File

@@ -18,6 +18,7 @@ package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Set; import java.util.Set;
import okio.Buffer; import okio.Buffer;
import okio.BufferedSink; import okio.BufferedSink;
@@ -56,7 +57,15 @@ public abstract class JsonAdapter<T> {
/** /**
* Encodes {@code value} as a Java model comprised of maps, lists, strings, numbers, booleans * Encodes {@code value} as a Java model comprised of maps, lists, strings, numbers, booleans
* and nulls. The returned model is equivalent to calling {@link #toJson} to encode {@code value} * and nulls.
*
* <p>Values encoded using {@code value(double)} or {@code value(long)} are modeled with the
* corresponding boxed type. Values encoded using {@code value(Number)} are modeled as a
* {@link Long} for boxed integer types ({@link Byte}, {@link Short}, {@link Integer}, and {@link
* Long}), as a {@link Double} for boxed floating point types ({@link Float} and {@link Double}),
* and as a {@link BigDecimal} for all other types.
*
* <p>The returned model is equivalent to calling {@link #toJson} to encode {@code value}
* as a JSON string, and then parsing that string without any particular type. * as a JSON string, and then parsing that string without any particular type.
*/ */
public final Object toJsonObject(T value) { public final Object toJsonObject(T value) {

View File

@@ -16,6 +16,7 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -24,6 +25,8 @@ import static com.squareup.moshi.JsonScope.EMPTY_ARRAY;
import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT; import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT;
import static com.squareup.moshi.JsonScope.EMPTY_OBJECT; import static com.squareup.moshi.JsonScope.EMPTY_OBJECT;
import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT; import static com.squareup.moshi.JsonScope.NONEMPTY_DOCUMENT;
import static java.lang.Double.NEGATIVE_INFINITY;
import static java.lang.Double.POSITIVE_INFINITY;
/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */ /** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */
final class ObjectJsonWriter extends JsonWriter { final class ObjectJsonWriter extends JsonWriter {
@@ -131,7 +134,16 @@ final class ObjectJsonWriter extends JsonWriter {
} }
@Override public JsonWriter value(double value) throws IOException { @Override public JsonWriter value(double value) throws IOException {
return value(Double.valueOf(value)); if (!lenient
&& (Double.isNaN(value) || value == NEGATIVE_INFINITY || value == POSITIVE_INFINITY)) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
}
if (promoteValueToName) {
return name(Double.toString(value));
}
add(value);
pathIndices[stackSize - 1]++;
return this;
} }
@Override public JsonWriter value(long value) throws IOException { @Override public JsonWriter value(long value) throws IOException {
@@ -144,16 +156,27 @@ final class ObjectJsonWriter extends JsonWriter {
} }
@Override public JsonWriter value(Number value) throws IOException { @Override public JsonWriter value(Number value) throws IOException {
if (!lenient) { // If it's trivially converted to a long, do that.
double d = value.doubleValue(); if (value instanceof Byte
if (d == Double.POSITIVE_INFINITY || d == Double.NEGATIVE_INFINITY || Double.isNaN(d)) { || value instanceof Short
throw new IllegalArgumentException("Numeric values must be finite, but was " + value); || value instanceof Integer
} || value instanceof Long) {
return value(value.longValue());
} }
// If it's trivially converted to a double, do that.
if (value instanceof Float || value instanceof Double) {
return value(value.doubleValue());
}
// Everything else gets converted to a BigDecimal.
BigDecimal bigDecimalValue = value instanceof BigDecimal
? ((BigDecimal) value)
: new BigDecimal(value.toString());
if (promoteValueToName) { if (promoteValueToName) {
return name(value.toString()); return name(bigDecimalValue.toString());
} }
add(value); add(bigDecimalValue);
pathIndices[stackSize - 1]++; pathIndices[stackSize - 1]++;
return this; return this;
} }

View File

@@ -16,8 +16,13 @@
package com.squareup.moshi; package com.squareup.moshi;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import org.junit.Test; import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@@ -66,5 +71,201 @@ public final class ObjectJsonWriterTest {
"Map key 'a' has multiple values at path $.a: 1 and 2"); "Map key 'a' has multiple values at path $.a: 1 and 2");
} }
} }
@Test public void valueLongEmitsLong() throws Exception {
ObjectJsonWriter writer = new ObjectJsonWriter();
writer.beginArray();
writer.value(Long.MIN_VALUE);
writer.value(-1L);
writer.value(0L);
writer.value(1L);
writer.value(Long.MAX_VALUE);
writer.endArray();
List<Number> numbers = Arrays.<Number>asList(
Long.MIN_VALUE,
-1L,
0L,
1L,
Long.MAX_VALUE);
assertThat((List<?>) writer.root()).isEqualTo(numbers);
}
@Test public void valueDoubleEmitsDouble() throws Exception {
ObjectJsonWriter writer = new ObjectJsonWriter();
writer.setLenient(true);
writer.beginArray();
writer.value(-2147483649.0d);
writer.value(-2147483648.0d);
writer.value(-1.0d);
writer.value(0.0d);
writer.value(1.0d);
writer.value(2147483647.0d);
writer.value(2147483648.0d);
writer.value(9007199254740991.0d);
writer.value(9007199254740992.0d);
writer.value(9007199254740994.0d);
writer.value(9223372036854776832.0d);
writer.value(-0.5d);
writer.value(-0.0d);
writer.value(0.5d);
writer.value(9.22337203685478e18);
writer.value(Double.NEGATIVE_INFINITY);
writer.value(Double.MIN_VALUE);
writer.value(Double.MIN_NORMAL);
writer.value(-Double.MIN_NORMAL);
writer.value(Double.MAX_VALUE);
writer.value(Double.POSITIVE_INFINITY);
writer.value(Double.NaN);
writer.endArray();
List<Number> numbers = Arrays.<Number>asList(
-2147483649.0d,
-2147483648.0d,
-1.0d,
0.0d,
1.0d,
2147483647.0d,
2147483648.0d,
9007199254740991.0d,
9007199254740992.0d,
9007199254740994.0d,
9223372036854775807.0d,
-0.5d,
-0.0d,
0.5d,
9.22337203685478e18,
Double.NEGATIVE_INFINITY,
Double.MIN_VALUE,
Double.MIN_NORMAL,
-Double.MIN_NORMAL,
Double.MAX_VALUE,
Double.POSITIVE_INFINITY,
Double.NaN);
assertThat((List<?>) writer.root()).isEqualTo(numbers);
}
@Test public void primitiveIntegerTypesEmitLong() throws Exception {
ObjectJsonWriter writer = new ObjectJsonWriter();
writer.beginArray();
writer.value(new Byte(Byte.MIN_VALUE));
writer.value(new Short(Short.MIN_VALUE));
writer.value(new Integer(Integer.MIN_VALUE));
writer.value(new Long(Long.MIN_VALUE));
writer.endArray();
List<Number> numbers = Arrays.<Number>asList(
-128L,
-32768L,
-2147483648L,
-9223372036854775808L);
assertThat((List<?>) writer.root()).isEqualTo(numbers);
}
@Test public void primitiveFloatingPointTypesEmitDouble() throws Exception {
ObjectJsonWriter writer = new ObjectJsonWriter();
writer.beginArray();
writer.value(new Float(0.5f));
writer.value(new Double(0.5d));
writer.endArray();
List<Number> numbers = Arrays.<Number>asList(
0.5d,
0.5d);
assertThat((List<?>) writer.root()).isEqualTo(numbers);
}
@Test public void otherNumberTypesEmitBigDecimal() throws Exception {
ObjectJsonWriter writer = new ObjectJsonWriter();
writer.beginArray();
writer.value(new AtomicInteger(-2147483648));
writer.value(new AtomicLong(-9223372036854775808L));
writer.value(new BigInteger("-9223372036854775808"));
writer.value(new BigInteger("-1"));
writer.value(new BigInteger("0"));
writer.value(new BigInteger("1"));
writer.value(new BigInteger("9223372036854775807"));
writer.value(new BigDecimal("-9223372036854775808"));
writer.value(new BigDecimal("-1"));
writer.value(new BigDecimal("0"));
writer.value(new BigDecimal("1"));
writer.value(new BigDecimal("9223372036854775807"));
writer.value(new BigInteger("-9223372036854775809"));
writer.value(new BigInteger("9223372036854775808"));
writer.value(new BigDecimal("-9223372036854775809"));
writer.value(new BigDecimal("9223372036854775808"));
writer.value(new BigDecimal("0.5"));
writer.value(new BigDecimal("100000e15"));
writer.value(new BigDecimal("0.0000100e-10"));
writer.endArray();
List<Number> numbers = Arrays.<Number>asList(
new BigDecimal("-2147483648"),
new BigDecimal("-9223372036854775808"),
new BigDecimal("-9223372036854775808"),
new BigDecimal("-1"),
new BigDecimal("0"),
new BigDecimal("1"),
new BigDecimal("9223372036854775807"),
new BigDecimal("-9223372036854775808"),
new BigDecimal("-1"),
new BigDecimal("0"),
new BigDecimal("1"),
new BigDecimal("9223372036854775807"),
new BigDecimal("-9223372036854775809"),
new BigDecimal("9223372036854775808"),
new BigDecimal("-9223372036854775809"),
new BigDecimal("9223372036854775808"),
new BigDecimal("0.5"),
new BigDecimal("100000e15"),
new BigDecimal("0.0000100e-10"));
assertThat((List<?>) writer.root()).isEqualTo(numbers);
}
@Test public void valueCustomNumberTypeEmitsLongOrBigDecimal() throws Exception {
ObjectJsonWriter writer = new ObjectJsonWriter();
writer.beginArray();
writer.value(stringNumber("-9223372036854775809"));
writer.value(stringNumber("-9223372036854775808"));
writer.value(stringNumber("0.5"));
writer.value(stringNumber("1.0"));
writer.endArray();
List<Number> numbers = Arrays.<Number>asList(
new BigDecimal("-9223372036854775809"),
new BigDecimal("-9223372036854775808"),
new BigDecimal("0.5"),
new BigDecimal("1.0"));
assertThat((List<?>) writer.root()).isEqualTo(numbers);
}
/**
* Returns an instance of number whose {@link #toString} is {@code s}. Using the standard number
* methods like {@link Number#doubleValue} are awkward because they may truncate or discard
* precision.
*/
private Number stringNumber(final String s) {
return new Number() {
@Override public int intValue() {
throw new AssertionError();
}
@Override public long longValue() {
throw new AssertionError();
}
@Override public float floatValue() {
throw new AssertionError();
}
@Override public double doubleValue() {
throw new AssertionError();
}
@Override public String toString() {
return s;
}
};
}
} }