Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NumberFormat support #4273

Open
wants to merge 4 commits into
base: 2.17
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;

import com.fasterxml.jackson.annotation.JsonFormat;

Expand Down Expand Up @@ -38,13 +41,28 @@ public class NumberSerializer

protected final boolean _isInt;

/**
* @since 2.17
*/
protected final NumberFormat _format;

/**
* @since 2.5
*/
public NumberSerializer(Class<? extends Number> rawType) {
super(rawType, false);
// since this will NOT be constructed for Integer or Long, only case is:
_isInt = (rawType == BigInteger.class);
_format = null;
}

/**
* @since 2.17
*/
public NumberSerializer(NumberSerializer src, NumberFormat format) {
super(src);
_isInt = src._isInt;
_format = format;
}

@Override
Expand All @@ -55,6 +73,21 @@ public JsonSerializer<?> createContextual(SerializerProvider prov,
if (format != null) {
switch (format.getShape()) {
case STRING:
if (format.hasPattern()) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (format.hasPattern()) {
// [databind#1114] since 2.17 : Support @JsonFormat for String, numbers, using String.format()
if (format.hasPattern()) {

DecimalFormat decimalFormat;
try {
if (format.hasLocale()) {
decimalFormat = new DecimalFormat(format.getPattern(),
DecimalFormatSymbols.getInstance(format.getLocale()));
} else {
decimalFormat = new DecimalFormat(format.getPattern());
}
} catch (IllegalArgumentException e) {
return prov.reportBadDefinition(handledType(),
String.format("Invalid `DecimalFormat`: \"%s\"", format.getPattern()));
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
String.format("Invalid `DecimalFormat`: \"%s\"", format.getPattern()));
String.format("Invalid 'DecimalFormat': \"%s\"", format.getPattern()));

Can we use single quote instead of backtick? I looked around the codebase and in most cases (not all) single quote is used.

}
return new NumberSerializer(this, decimalFormat);
}
// [databind#2264]: Need special handling for `BigDecimal`
if (((Class<?>) handledType()) == BigDecimal.class) {
return bigDecimalAsStringSerializer();
Expand All @@ -69,6 +102,10 @@ public JsonSerializer<?> createContextual(SerializerProvider prov,
@Override
public void serialize(Number value, JsonGenerator g, SerializerProvider provider) throws IOException
{
if (_format != null) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (_format != null) {
// [databind#1114] since 2.17 : Support @JsonFormat for String, numbers, using String.format()
if (_format != null) {

g.writeString(_format.format(value));
return;
}
// should mostly come in as one of these two:
if (value instanceof BigDecimal) {
g.writeNumber((BigDecimal) value);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.fasterxml.jackson.databind.format;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.BaseMapTest;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Assert;

import java.math.BigDecimal;

import java.util.Locale;
import java.util.TimeZone;

public class NumberFormatTest extends BaseMapTest
{
protected static class NumberWrapper {

public BigDecimal value;

public NumberWrapper() {}
public NumberWrapper(BigDecimal v) { value = v; }
}

public void testTypeDefaults() throws Exception
{
ObjectMapper mapper = newJsonMapper();
mapper.configOverride(BigDecimal.class)
.setFormat(new JsonFormat.Value("00,000.00", JsonFormat.Shape.STRING, (Locale) null, (TimeZone) null, null, null));
String json = mapper.writeValueAsString(new NumberWrapper(new BigDecimal("1234")));
assertEquals(a2q("{'value':'01,234.00'}"), json);

// and then read back is not supported yet.
/*NumberWrapper w = mapper.readValue(a2q("{'value':'01,234.00'}"), NumberWrapper.class);
assertNotNull(w);
assertEquals(new BigDecimal("1234"), w.value);*/
}

protected static class InvalidPatternWrapper {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#,##0.#.#")
public BigDecimal value;

public InvalidPatternWrapper(BigDecimal value) {
this.value = value;
}
}

public void testInvalidPattern() throws Exception {
ObjectMapper mapper = newJsonMapper();
Assert.assertThrows(JsonMappingException.class, () -> {
Copy link
Member

Choose a reason for hiding this comment

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

Could you check the exception message too (so it's the expected exception)

mapper.writeValueAsString(new InvalidPatternWrapper(BigDecimal.ZERO));
});
}
Comment on lines +24 to +52
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
public void testTypeDefaults() throws Exception
{
ObjectMapper mapper = newJsonMapper();
mapper.configOverride(BigDecimal.class)
.setFormat(new JsonFormat.Value("00,000.00", JsonFormat.Shape.STRING, (Locale) null, (TimeZone) null, null, null));
String json = mapper.writeValueAsString(new NumberWrapper(new BigDecimal("1234")));
assertEquals(a2q("{'value':'01,234.00'}"), json);
// and then read back is not supported yet.
/*NumberWrapper w = mapper.readValue(a2q("{'value':'01,234.00'}"), NumberWrapper.class);
assertNotNull(w);
assertEquals(new BigDecimal("1234"), w.value);*/
}
protected static class InvalidPatternWrapper {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#,##0.#.#")
public BigDecimal value;
public InvalidPatternWrapper(BigDecimal value) {
this.value = value;
}
}
public void testInvalidPattern() throws Exception {
ObjectMapper mapper = newJsonMapper();
Assert.assertThrows(JsonMappingException.class, () -> {
mapper.writeValueAsString(new InvalidPatternWrapper(BigDecimal.ZERO));
});
}
protected static class InvalidPatternWrapper {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "#,##0.#.#")
public BigDecimal value;
public InvalidPatternWrapper(BigDecimal value) {
this.value = value;
}
}
@Test
public void testTypeDefaults() throws Exception
{
ObjectMapper mapper = newJsonMapper();
mapper.configOverride(BigDecimal.class)
.setFormat(new JsonFormat.Value("00,000.00", JsonFormat.Shape.STRING, (Locale) null, (TimeZone) null, null, null));
String json = mapper.writeValueAsString(new NumberWrapper(new BigDecimal("1234")));
assertEquals(a2q("{'value':'01,234.00'}"), json);
// and then read back is not supported yet.
/*NumberWrapper w = mapper.readValue(a2q("{'value':'01,234.00'}"), NumberWrapper.class);
assertNotNull(w);
assertEquals(new BigDecimal("1234"), w.value);*/
}
@Test
public void testInvalidPattern() throws Exception {
ObjectMapper mapper = newJsonMapper();
try {
mapper.writeValueAsString(new InvalidPatternWrapper(BigDecimal.ZERO));
fail();
} catch (JsonMappingException e) {
verifyException(e, "expected message part 1/2"); //
verifyException(e, "expected message part 2/2");
}
}

Couple of suggestion on test if that's okay with you 🙂

  1. Let's use JUnit 5, (meaning remove extends BaseMapTest
  2. Class declaration comes first then test methods (Check suggestion)
  3. As suggested above, check message by using try-catch and verifyException (from DatabindTestUtil) combo

Copy link
Member

Choose a reason for hiding this comment

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

Above comment seems like Jackson Coding Style convention idea 🤔... WDYT, @cowtowncoder ?

}