diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java b/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java index e85867ad..0ade8b35 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java @@ -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 JSON.parse() 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. *

* 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. @@ -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 @@ -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 @@ -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) { @@ -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) { @@ -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; @@ -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); } /** @@ -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; @@ -662,21 +734,21 @@ 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); } /** @@ -684,7 +756,15 @@ public static Value forShape(Shape sh) { */ 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); } /** @@ -692,7 +772,7 @@ public static Value forLeniency(boolean lenient) { */ public Value withPattern(String p) { return new Value(p, _shape, _locale, _timezoneStr, _timezone, - _features, _lenient); + _features, _lenient, _radix); } /** @@ -703,7 +783,7 @@ public Value withShape(Shape s) { return this; } return new Value(_pattern, s, _locale, _timezoneStr, _timezone, - _features, _lenient); + _features, _lenient, _radix); } /** @@ -711,7 +791,7 @@ public Value withShape(Shape s) { */ public Value withLocale(Locale l) { return new Value(_pattern, _shape, l, _timezoneStr, _timezone, - _features, _lenient); + _features, _lenient, _radix); } /** @@ -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); } /** @@ -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); } /** @@ -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 @@ -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 *

@@ -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
@@ -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
@@ -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);
         }
     }
 }
diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
index 0a3efb58..ef5c4ce5 100644
--- a/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
+++ b/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
@@ -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.*;
 
 /**
@@ -30,6 +31,7 @@ public void testEmptyInstanceDefaults() {
         assertFalse(empty.hasShape());
         assertFalse(empty.hasTimeZone());
         assertFalse(empty.hasLenient());
+        assertFalse(empty.hasNonDefaultRadix());
 
         assertFalse(empty.isLenient());
     }
@@ -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());
     }
 
@@ -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());
+    }
 }