Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,12 @@ private MapperConverter findConverter(final boolean copyDate, final AccessMode.D

if (Date.class.isAssignableFrom(type) && copyDate) {
converter = new DateWithCopyConverter(Adapter.class.cast(adapters.get(new AdapterKey(Date.class, String.class))));
} else if (type == BigDecimal.class || type == BigInteger.class) {
// BigDecimal/BigInteger are "primitives" in the mapper so they bypass
// config.findAdapter() in writeValue(). Use a direct get() to trigger
// lazy loading and respect useBigDecimalStringAdapter/useBigIntegerStringAdapter.
// this makes it symetric with READ
converter = adapters.get(new AdapterKey(type, String.class));
} else {
for (final Map.Entry<AdapterKey, Adapter<?, ?>> adapterEntry : adapters.entrySet()) {
if (adapterEntry.getKey().getFrom() == adapterEntry.getKey().getTo()) { // String -> String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
public class BigDecimalConverter implements Converter<BigDecimal> {
@Override
public String toString(final BigDecimal instance) {
return instance.toString();
// when using the converter, user expects the decimal notation
// otherwise, JsonNumber will give the E (scientific) notation
return instance.toPlainString();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm, this one was expected originally
i'm not more 100% sure but think the length of the value can be too wide (no more sure if IEEE 754 or nother spec) and therefore not portable accross parsers whereas scientific notation was more portable
can be sane to keep the default in mapper (same for the toggles, no reason to break mapper for JSON-B there)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, no reason to write a string version of the E notation if this is already the default for JsonNumber. It's a String here, so I think it makes sense.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there are a few reasons

  • being symmetric and aligned on other numbers (parse/toString)
  • being potentially shorter for long occurences and save chars
  • being aligned on the number flavor

but agree it is not crazy but also probably not worth it, no?

}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ public Object from(final Object a) {

private boolean useShortISO8601Format = true;
private DateTimeFormatter dateTimeFormatter;
private boolean useBigIntegerStringAdapter = true;
private boolean useBigDecimalStringAdapter = true;
private boolean useBigIntegerStringAdapter = false; // Jakarta JSON-B 3.0 Section 3.4.1 (BigDecimal MUST be a JSON number)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this default is super important, it is the only guarantee you can have round trips, disabling it make the read/write polymorphic when you overflow supported number range which is totally broken from a typed pr parser point of view

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In reality, I did not change the default, I made them respect the behavior we had :-)
Leaving them to true breaks tons of tests, because it was not used before (swapped properties and Mappings.java skipping the adapter for write)

The "breaking change" is purely theoretical here.
RFC 8259 and Jakarta JSON-B both expect BigDecimal to be a JSON number. So it is just reflecting the way it was working before.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record it got broken in johnzon 1.2.21, before it was working as expected and as set in the code (string).

JSON-B intoduced this broken behavior (likely trying to get JSON-B compliant since they broke themselves) to consider BigDeciman a numbers (which makes it literally not interoperable with JS for example and some other parsers) and totally inconsistent with I-JSON support of the spec, but johnzon-mapper should keep its default (string) which is the only portable way to handle big numbers since they cant be parsed by common numbers (even on 64 bits systems).

So you didn't break it in this PR but it got broken 2 years ago to make it epxlicit.

So your system property is a good compromise to make both cases workable without a code review since it can break easily in prod (we rarely test real "Big" ranges in unit tests)

private boolean useBigDecimalStringAdapter = false; // Jakarta JSON-B 3.0 Section 3.4.1 (BigDecimal MUST be a JSON number)

public void setUseShortISO8601Format(final boolean useShortISO8601Format) {
this.useShortISO8601Format = useShortISO8601Format;
Expand Down Expand Up @@ -163,10 +163,10 @@ public Set<AdapterKey> adapterKeys() {
if (from == String.class) {
return add(key, new ConverterAdapter<>(new StringConverter(), String.class));
}
if (from == BigDecimal.class && useBigIntegerStringAdapter) {
if (from == BigDecimal.class && useBigDecimalStringAdapter) {
return add(key, new ConverterAdapter<>(new BigDecimalConverter(), BigDecimal.class));
}
if (from == BigInteger.class && useBigDecimalStringAdapter) {
if (from == BigInteger.class && useBigIntegerStringAdapter) {
return add(key, new ConverterAdapter<>(new BigIntegerConverter(), BigInteger.class));
}
if (from == Locale.class) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@
package org.apache.johnzon.mapper;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.math.BigDecimal;
import java.math.BigInteger;

import org.apache.johnzon.mapper.converter.BigDecimalConverter;
import org.apache.johnzon.mapper.internal.AdapterKey;
import org.apache.johnzon.mapper.map.LazyConverterMap;
import org.junit.Test;

public class NumberSerializationTest {
Expand All @@ -46,11 +52,91 @@ public void numberFromJson() {
mapper.close();
}

/**
* Bug: BigDecimalConverter used toString() which produces scientific notation
* (e.g. "7.33915E-7"). Should use toPlainString() to produce "0.000000733915".
*/
@Test
public void bigDecimalConverterUsesPlainNotation() {
final BigDecimalConverter converter = new BigDecimalConverter();
final BigDecimal smallValue = new BigDecimal("0.000000733915");
final String result = converter.toString(smallValue);
assertEquals("BigDecimalConverter should use plain notation, not scientific",
"0.000000733915", result);
}

/**
* Bug fix: useBigDecimalStringAdapter and useBigIntegerStringAdapter flags
* were swapped in LazyConverterMap. Each flag must control its own type.
* Both default to false (JSON number).
*/
@Test
public void bigDecimalStringAdapterFlagControlsBigDecimal() {
// Default: BigDecimal adapter is OFF (useBigDecimalStringAdapter=false)
final LazyConverterMap defaultAdapters = new LazyConverterMap();
assertNull("BigDecimal adapter should be null by default",
defaultAdapters.get(new AdapterKey(BigDecimal.class, String.class)));
// Enabled: BigDecimal adapter is ON (fresh instance to avoid NO_ADAPTER cache)
final LazyConverterMap enabledAdapters = new LazyConverterMap();
enabledAdapters.setUseBigDecimalStringAdapter(true);
assertNotNull("BigDecimal adapter should be active when flag is true",
enabledAdapters.get(new AdapterKey(BigDecimal.class, String.class)));
}

@Test
public void bigIntegerStringAdapterFlagControlsBigInteger() {
final LazyConverterMap adapters = new LazyConverterMap();
// Default: BigInteger adapter is OFF (useBigIntegerStringAdapter=false)
assertNull("BigInteger adapter should be null by default",
adapters.get(new AdapterKey(BigInteger.class, String.class)));
// Enable it explicitly
final LazyConverterMap adapters2 = new LazyConverterMap();
adapters2.setUseBigIntegerStringAdapter(true);
assertNotNull("BigInteger adapter should be active when flag is true",
adapters2.get(new AdapterKey(BigInteger.class, String.class)));
}

/**
* With useBigDecimalStringAdapter=false (default), BigDecimal fields
* should be serialized as JSON numbers in the mapper.
*/
@Test
public void bigDecimalDefaultSerializesAsNumber() {
try (final Mapper mapper = new MapperBuilder().build()) {
final BigDecimalHolder holder = new BigDecimalHolder();
holder.score = new BigDecimal("0.000000733915");
final String json = mapper.writeObjectAsString(holder);
// Default: BigDecimal as JSON number (per JSON-B spec and RFC 8259)
assertEquals("{\"score\":7.33915E-7}", json);
}
}

/**
* With useBigDecimalStringAdapter=true, BigDecimal fields should be
* serialized as JSON strings using plain notation (symmetric with deserialization).
*/
@Test
public void bigDecimalWithStringAdapterSerializesAsString() {
try (final Mapper mapper = new MapperBuilder()
.setUseBigDecimalStringAdapter(true)
.build()) {
final BigDecimalHolder holder = new BigDecimalHolder();
holder.score = new BigDecimal("0.000000733915");
final String json = mapper.writeObjectAsString(holder);
// String adapter uses toPlainString(), no scientific notation
assertEquals("{\"score\":\"0.000000733915\"}", json);
}
}

public static class Holder {
public long value;
}

public static class Num {
public Number value;
}

public static class BigDecimalHolder {
public BigDecimal score;
}
}
Loading