Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 125 additions & 17 deletions src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
* This is useful to prevent large numeric values from being rounded to their closest double
* values when deserialized by JSON parsers (for instance <code>JSON.parse()</code> in web
* browsers) that do not support numbers with more than 53 bits of precision.
* When serializing {@link java.lang.Number} to a string, it is possible to specify radix,
* the numeric base used to output the number in.
* <p>
* They can also be serialized to full objects if {@link Shape#OBJECT} is used.
* Otherwise, the default behavior of serializing to a scalar number value will be preferred.
Expand Down Expand Up @@ -78,6 +80,13 @@
*/
public final static String DEFAULT_TIMEZONE = "##default";

/**
* This is a marker signaling that a configured default radix should be used, which typically means 10,
* when serializing {@link java.lang.Number} properties with {@link Shape#STRING}.
* @since 2.21
*/
public final static int DEFAULT_RADIX = -1;

/**
* Datatype-specific additional piece of configuration that may be used
* to further refine formatting aspects. This may, for example, determine
Expand Down Expand Up @@ -126,6 +135,16 @@
*/
public OptBoolean lenient() default OptBoolean.DEFAULT;

/**
* Property that indicates the numeric base used to output {@link java.lang.Number} properties when {@link Shape#STRING}
* is specified.
* For example, if 2 is used, then the output will be a binary representation of a number as a string,
* and with 16, the number will be outputted in the hexadecimal form.
*
* @since 2.21
*/
public int radix() default DEFAULT_RADIX;

/**
* Set of {@link JsonFormat.Feature}s to explicitly enable with respect
* to handling of annotated property. This will have precedence over possible
Expand Down Expand Up @@ -518,21 +537,41 @@ public static class Value
*/
private final Features _features;

/**
* @since 2.21
*/
private final int _radix;

// lazily constructed when created from annotations
private transient TimeZone _timezone;

public Value() {
this("", Shape.ANY, "", "", Features.empty(), null);
this("", Shape.ANY, "", "", Features.empty(), null, DEFAULT_RADIX);
}

public Value(JsonFormat ann) {
this(ann.pattern(), ann.shape(), ann.locale(), ann.timezone(),
Features.construct(ann), ann.lenient().asBoolean());
Features.construct(ann), ann.lenient().asBoolean(), ann.radix());
}

/**
* @since 2.21
*/
public Value(String p, Shape sh, String localeStr, String tzStr, Features f,
Boolean lenient, int radix)
{
this(p, sh,
(localeStr == null || localeStr.length() == 0 || DEFAULT_LOCALE.equals(localeStr)) ?
null : new Locale(localeStr),
(tzStr == null || tzStr.length() == 0 || DEFAULT_TIMEZONE.equals(tzStr)) ?
null : tzStr,
null, f, lenient, radix);
}

/**
* @since 2.9
*/
@Deprecated //since 2.21
public Value(String p, Shape sh, String localeStr, String tzStr, Features f,
Boolean lenient)
{
Expand All @@ -544,9 +583,26 @@ public Value(String p, Shape sh, String localeStr, String tzStr, Features f,
null, f, lenient);
}

/**
* @since 2.21
*/
public Value(String p, Shape sh, Locale l, TimeZone tz, Features f,
Boolean lenient, int radix)
{
_pattern = (p == null) ? "" : p;
_shape = (sh == null) ? Shape.ANY : sh;
_locale = l;
_timezone = tz;
_timezoneStr = null;
_features = (f == null) ? Features.empty() : f;
_lenient = lenient;
_radix = radix;
}

/**
* @since 2.9
*/
@Deprecated //since 2.21
public Value(String p, Shape sh, Locale l, TimeZone tz, Features f,
Boolean lenient)
{
Expand All @@ -557,13 +613,14 @@ public Value(String p, Shape sh, Locale l, TimeZone tz, Features f,
_timezoneStr = null;
_features = (f == null) ? Features.empty() : f;
_lenient = lenient;
_radix = DEFAULT_RADIX;
}

/**
* @since 2.9
* @since 2.21
*/
public Value(String p, Shape sh, Locale l, String tzStr, TimeZone tz, Features f,
Boolean lenient)
Boolean lenient, int radix)
{
_pattern = (p == null) ? "" : p;
_shape = (sh == null) ? Shape.ANY : sh;
Expand All @@ -572,6 +629,17 @@ public Value(String p, Shape sh, Locale l, String tzStr, TimeZone tz, Features f
_timezoneStr = tzStr;
_features = (f == null) ? Features.empty() : f;
_lenient = lenient;
_radix = radix;
}

/**
* @since 2.9
*/
@Deprecated //since 2.21
public Value(String p, Shape sh, Locale l, String tzStr, TimeZone tz, Features f,
Boolean lenient)
{
this(p, sh, l, tzStr, tz, f, lenient, DEFAULT_RADIX);
}

/**
Expand Down Expand Up @@ -651,6 +719,10 @@ public final Value withOverrides(Value overrides) {
if (lenient == null) {
lenient = _lenient;
}
int radix = overrides._radix;
if(radix == DEFAULT_RADIX) {
radix = _radix;
}

// timezone not merged, just choose one
String tzStr = overrides._timezoneStr;
Expand All @@ -662,37 +734,45 @@ public final Value withOverrides(Value overrides) {
} else {
tz = overrides._timezone;
}
return new Value(p, sh, l, tzStr, tz, f, lenient);
return new Value(p, sh, l, tzStr, tz, f, lenient, radix);
}

/**
* @since 2.6
*/
public static Value forPattern(String p) {
return new Value(p, null, null, null, null, Features.empty(), null);
return new Value(p, null, null, null, null, Features.empty(), null, DEFAULT_RADIX);
}

/**
* @since 2.7
*/
public static Value forShape(Shape sh) {
return new Value("", sh, null, null, null, Features.empty(), null);
return new Value("", sh, null, null, null, Features.empty(), null, DEFAULT_RADIX);
}

/**
* @since 2.9
*/
public static Value forLeniency(boolean lenient) {
return new Value("", null, null, null, null, Features.empty(),
Boolean.valueOf(lenient));
Boolean.valueOf(lenient), DEFAULT_RADIX);
}

/**
* @since 2.21
*/
public static Value forRadix(int radix) {
return new Value("", null, null, null, null, Features.empty(),
null, radix);
}

/**
* @since 2.1
*/
public Value withPattern(String p) {
return new Value(p, _shape, _locale, _timezoneStr, _timezone,
_features, _lenient);
_features, _lenient, _radix);
}

/**
Expand All @@ -703,15 +783,15 @@ public Value withShape(Shape s) {
return this;
}
return new Value(_pattern, s, _locale, _timezoneStr, _timezone,
_features, _lenient);
_features, _lenient, _radix);
}

/**
* @since 2.1
*/
public Value withLocale(Locale l) {
return new Value(_pattern, _shape, l, _timezoneStr, _timezone,
_features, _lenient);
_features, _lenient, _radix);
}

/**
Expand All @@ -730,7 +810,18 @@ public Value withLenient(Boolean lenient) {
return this;
}
return new Value(_pattern, _shape, _locale, _timezoneStr, _timezone,
_features, lenient);
_features, lenient, _radix);
}

/**
* @since 2.21
*/
public Value withRadix(int radix) {
if (radix == _radix) {
return this;
}
return new Value(_pattern, _shape, _locale, _timezoneStr, _timezone,
_features, _lenient, radix);
}

/**
Expand All @@ -740,7 +831,7 @@ public Value withFeature(JsonFormat.Feature f) {
Features newFeats = _features.with(f);
return (newFeats == _features) ? this :
new Value(_pattern, _shape, _locale, _timezoneStr, _timezone,
newFeats, _lenient);
newFeats, _lenient, _radix);
}

/**
Expand All @@ -750,7 +841,7 @@ public Value withoutFeature(JsonFormat.Feature f) {
Features newFeats = _features.without(f);
return (newFeats == _features) ? this :
new Value(_pattern, _shape, _locale, _timezoneStr, _timezone,
newFeats, _lenient);
newFeats, _lenient, _radix);
}

@Override
Expand All @@ -773,6 +864,13 @@ public Boolean getLenient() {
return _lenient;
}

/**
* @return radix to use for serializing subclasses of {@link Number} as strings.
* If set to -1, a custom radix has not been specified.
* @since 2.21
*/
public int getRadix() { return _radix; }

/**
* Convenience method equivalent to
*<pre>
Expand Down Expand Up @@ -848,6 +946,15 @@ public boolean hasLenient() {
return _lenient != null;
}

/**
* Accessor for checking whether non-default radix has been specified.
*
* @since 2.21
*/
public boolean hasNonDefaultRadix() {
return _radix != DEFAULT_RADIX;
}

/**
* Accessor for checking whether this format value has specific setting for
* given feature. Result is 3-valued with either `null`, {@link Boolean#TRUE} or
Expand All @@ -872,8 +979,8 @@ public Features getFeatures() {

@Override
public String toString() {
return String.format("JsonFormat.Value(pattern=%s,shape=%s,lenient=%s,locale=%s,timezone=%s,features=%s)",
_pattern, _shape, _lenient, _locale, _timezoneStr, _features);
return String.format("JsonFormat.Value(pattern=%s,shape=%s,lenient=%s,locale=%s,timezone=%s,features=%s,radix=%s)",
_pattern, _shape, _lenient, _locale, _timezoneStr, _features, _radix);
}

@Override
Expand Down Expand Up @@ -908,7 +1015,8 @@ public boolean equals(Object o) {
&& Objects.equals(_timezoneStr, other._timezoneStr)
&& Objects.equals(_pattern, other._pattern)
&& Objects.equals(_timezone, other._timezone)
&& Objects.equals(_locale, other._locale);
&& Objects.equals(_locale, other._locale)
&& Objects.equals(_radix, other._radix);
}
}
}
28 changes: 26 additions & 2 deletions src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import org.junit.jupiter.api.Test;

import static com.fasterxml.jackson.annotation.JsonFormat.DEFAULT_RADIX;
import static org.junit.jupiter.api.Assertions.*;

/**
Expand All @@ -30,6 +31,7 @@ public void testEmptyInstanceDefaults() {
assertFalse(empty.hasShape());
assertFalse(empty.hasTimeZone());
assertFalse(empty.hasLenient());
assertFalse(empty.hasNonDefaultRadix());

assertFalse(empty.isLenient());
}
Expand Down Expand Up @@ -63,9 +65,9 @@ public void testEquality() {

@Test
public void testToString() {
assertEquals("JsonFormat.Value(pattern=,shape=STRING,lenient=null,locale=null,timezone=null,features=EMPTY)",
assertEquals("JsonFormat.Value(pattern=,shape=STRING,lenient=null,locale=null,timezone=null,features=EMPTY,radix=-1)",
JsonFormat.Value.forShape(JsonFormat.Shape.STRING).toString());
assertEquals("JsonFormat.Value(pattern=[.],shape=ANY,lenient=null,locale=null,timezone=null,features=EMPTY)",
assertEquals("JsonFormat.Value(pattern=[.],shape=ANY,lenient=null,locale=null,timezone=null,features=EMPTY,radix=-1)",
JsonFormat.Value.forPattern("[.]").toString());
}

Expand Down Expand Up @@ -260,4 +262,26 @@ public void testFeatures() {
assertEquals(Boolean.FALSE, f4.get(Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY));
assertEquals(Boolean.TRUE, f4.get(Feature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS));
}

@Test
void testRadix() {
//Non-Default radix overrides the default
int binaryRadix = 2;
final JsonFormat.Value v = JsonFormat.Value.forRadix(binaryRadix);
JsonFormat.Value merged = EMPTY.withOverrides(v);
assertEquals(DEFAULT_RADIX, EMPTY.getRadix());
assertEquals(binaryRadix, merged.getRadix());

//Default does not override
final JsonFormat.Value v2 = JsonFormat.Value.forRadix(binaryRadix);
merged = v2.withOverrides(EMPTY);
assertEquals(binaryRadix, v2.getRadix());
assertEquals(binaryRadix, merged.getRadix());

JsonFormat.Value emptyWithBinaryRadix = EMPTY.withRadix(binaryRadix);
assertEquals(binaryRadix, emptyWithBinaryRadix.getRadix());

JsonFormat.Value forBinaryRadix = JsonFormat.Value.forRadix(binaryRadix);
assertEquals(binaryRadix, forBinaryRadix.getRadix());
}
}