From 4cffce0717ace177226f5913c40cabb9522c70d9 Mon Sep 17 00:00:00 2001 From: Davyd Fridman Date: Mon, 29 Sep 2025 21:29:27 -0500 Subject: [PATCH 1/4] Add radix property to JsonFormat (#320) --- .../jackson/annotation/JsonFormat.java | 135 +++++++++++++++--- .../jackson/annotation/JsonFormatTest.java | 14 +- 2 files changed, 130 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java b/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java index e85867ad..201f5737 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,12 @@ */ public final static String DEFAULT_TIMEZONE = "##default"; + /** + * Value that indicates the default radix(numeric base) to use for outputting {@link java.lang.Number} properties + * when {@link Shape#STRING} is specified. + */ + public final static byte DEFAULT_RADIX = 10; + /** * Datatype-specific additional piece of configuration that may be used * to further refine formatting aspects. This may, for example, determine @@ -126,6 +134,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 byte 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 +536,41 @@ public static class Value */ private final Features _features; + /** + * @since 2.21 + */ + private final byte _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, byte 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 +582,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, byte 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 +612,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, byte radix) { _pattern = (p == null) ? "" : p; _shape = (sh == null) ? Shape.ANY : sh; @@ -572,6 +628,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); } /** @@ -662,21 +729,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, overrides._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 +751,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(byte radix) { + return new Value("", null, null, null, null, Features.empty(), + null, radix); } /** @@ -692,7 +767,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 +778,7 @@ public Value withShape(Shape s) { return this; } return new Value(_pattern, s, _locale, _timezoneStr, _timezone, - _features, _lenient); + _features, _lenient, _radix); } /** @@ -711,7 +786,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 +805,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(byte radix) { + if (radix == _radix) { + return this; + } + return new Value(_pattern, _shape, _locale, _timezoneStr, _timezone, + _features, _lenient, radix); } /** @@ -740,7 +826,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 +836,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 +859,11 @@ public Boolean getLenient() { return _lenient; } + /** + * @since 2.21 + */ + public byte getRadix() { return _radix; } + /** * Convenience method equivalent to *

@@ -848,6 +939,15 @@ public boolean hasLenient() {
             return _lenient != null;
         }
 
+        /**
+         * Accessor for checking whether non-default (non-10) 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 +972,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 +1008,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..4ccbcd3c 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=10)",
                 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=10)",
                 JsonFormat.Value.forPattern("[.]").toString());
     }
 
@@ -146,6 +148,14 @@ public void testSimpleMerge()
         assertFalse(merged.hasLocale());
         assertEquals(TEST_SHAPE, merged.getShape());
         assertFalse(merged.hasTimeZone());
+
+        //radix always overrides
+        byte binaryRadix = 2;
+        final JsonFormat.Value v3 = JsonFormat.Value.forRadix(binaryRadix);
+        merged = EMPTY.withOverrides(v3);
+        assertEquals(DEFAULT_RADIX, EMPTY.getRadix());
+        assertEquals(binaryRadix, merged.getRadix());
+
     }
 
     @Test

From 8ca3bf3333852f59ce5788ec5d64fe959b1c4f7f Mon Sep 17 00:00:00 2001
From: Davyd Fridman 
Date: Tue, 30 Sep 2025 11:43:09 -0500
Subject: [PATCH 2/4] Use a special marker default for radix in JsonFormat
 (#320)

---
 .../jackson/annotation/JsonFormat.java        | 33 ++++++++++++-------
 .../jackson/annotation/JsonFormatTest.java    | 30 ++++++++++++-----
 2 files changed, 43 insertions(+), 20 deletions(-)

diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java b/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java
index 201f5737..017b2b88 100644
--- a/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java
+++ b/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java
@@ -81,10 +81,13 @@
     public final static String DEFAULT_TIMEZONE = "##default";
 
     /**
-     * Value that indicates the default radix(numeric base) to use for outputting {@link java.lang.Number} properties
-     * when {@link Shape#STRING} is specified.
+     * Value that indicates the default radix(numeric base) should be used when outputting
+     * {@link java.lang.Number} properties when {@link Shape#STRING} is specified.
+     * This is a marker signaling that {@link JsonFormat.Value} with this radix should
+     * not override the radix of another {@link JsonFormat.Value}.
+     * @since 2.21
      */
-    public final static byte DEFAULT_RADIX = 10;
+    public final static int DEFAULT_RADIX = -1;
 
     /**
      * Datatype-specific additional piece of configuration that may be used
@@ -142,7 +145,7 @@
      *
      * @since 2.21
      */
-    public byte radix() default DEFAULT_RADIX;
+    public int radix() default DEFAULT_RADIX;
 
     /**
      * Set of {@link JsonFormat.Feature}s to explicitly enable with respect
@@ -539,7 +542,7 @@ public static class Value
         /**
          * @since 2.21
          */
-        private final byte _radix;
+        private final int _radix;
 
         // lazily constructed when created from annotations
         private transient TimeZone _timezone;
@@ -557,7 +560,7 @@ public Value(JsonFormat ann) {
          * @since 2.21
          */
         public Value(String p, Shape sh, String localeStr, String tzStr, Features f,
-                Boolean lenient, byte radix)
+                Boolean lenient, int radix)
         {
             this(p, sh,
                     (localeStr == null || localeStr.length() == 0 || DEFAULT_LOCALE.equals(localeStr)) ?
@@ -586,7 +589,7 @@ public Value(String p, Shape sh, String localeStr, String tzStr, Features f,
          * @since 2.21
          */
         public Value(String p, Shape sh, Locale l, TimeZone tz, Features f,
-                Boolean lenient, byte radix)
+                Boolean lenient, int radix)
         {
             _pattern = (p == null) ? "" : p;
             _shape = (sh == null) ? Shape.ANY : sh;
@@ -619,7 +622,7 @@ public Value(String p, Shape sh, Locale l, TimeZone tz, Features f,
          * @since 2.21
          */
         public Value(String p, Shape sh, Locale l, String tzStr, TimeZone tz, Features f,
-                Boolean lenient, byte radix)
+                Boolean lenient, int radix)
         {
             _pattern = (p == null) ? "" : p;
             _shape = (sh == null) ? Shape.ANY : sh;
@@ -718,6 +721,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;
@@ -729,7 +736,7 @@ public final Value withOverrides(Value overrides) {
             } else {
                 tz = overrides._timezone;
             }
-            return new Value(p, sh, l, tzStr, tz, f, lenient, overrides._radix);
+            return new Value(p, sh, l, tzStr, tz, f, lenient, radix);
         }
 
         /**
@@ -860,9 +867,11 @@ public Boolean getLenient() {
         }
 
         /**
+         * @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 byte getRadix() { return _radix; }
+        public int getRadix() { return _radix; }
 
         /**
          * Convenience method equivalent to
@@ -940,12 +949,12 @@ public boolean hasLenient() {
         }
 
         /**
-         * Accessor for checking whether non-default (non-10) radix has been specified.
+         * Accessor for checking whether non-default (neither special default marker -1 nor 10) radix has been specified.
          *
          * @since 2.21
          */
         public boolean hasNonDefaultRadix() {
-            return _radix != DEFAULT_RADIX;
+            return _radix != DEFAULT_RADIX && _radix != 10;
         }
 
         /**
diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
index 4ccbcd3c..642e06ce 100644
--- a/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
+++ b/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
@@ -148,14 +148,6 @@ public void testSimpleMerge()
         assertFalse(merged.hasLocale());
         assertEquals(TEST_SHAPE, merged.getShape());
         assertFalse(merged.hasTimeZone());
-
-        //radix always overrides
-        byte binaryRadix = 2;
-        final JsonFormat.Value v3 = JsonFormat.Value.forRadix(binaryRadix);
-        merged = EMPTY.withOverrides(v3);
-        assertEquals(DEFAULT_RADIX, EMPTY.getRadix());
-        assertEquals(binaryRadix, merged.getRadix());
-
     }
 
     @Test
@@ -270,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
+        byte 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());
+    }
 }

From e0a0856729b2409e42b31c6185b222767d2d584f Mon Sep 17 00:00:00 2001
From: Davyd Fridman 
Date: Fri, 3 Oct 2025 10:25:10 -0500
Subject: [PATCH 3/4] Address PR comments (#320)

---
 .../fasterxml/jackson/annotation/JsonFormat.java   | 14 ++++++--------
 .../jackson/annotation/JsonFormatTest.java         |  4 ++--
 2 files changed, 8 insertions(+), 10 deletions(-)

diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java b/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java
index 017b2b88..0ade8b35 100644
--- a/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java
+++ b/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java
@@ -81,10 +81,8 @@
     public final static String DEFAULT_TIMEZONE = "##default";
 
     /**
-     * Value that indicates the default radix(numeric base) should be used when outputting
-     * {@link java.lang.Number} properties when {@link Shape#STRING} is specified.
-     * This is a marker signaling that {@link JsonFormat.Value} with this radix should
-     * not override the radix of another {@link JsonFormat.Value}.
+     * 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;
@@ -764,7 +762,7 @@ public static Value forLeniency(boolean lenient) {
         /**
         * @since 2.21
         */
-        public static Value forRadix(byte radix) {
+        public static Value forRadix(int radix) {
             return new Value("", null, null, null, null, Features.empty(),
                     null, radix);
         }
@@ -818,7 +816,7 @@ public Value withLenient(Boolean lenient) {
         /**
          * @since 2.21
          */
-        public Value withRadix(byte radix) {
+        public Value withRadix(int radix) {
             if (radix == _radix) {
                 return this;
             }
@@ -949,12 +947,12 @@ public boolean hasLenient() {
         }
 
         /**
-         * Accessor for checking whether non-default (neither special default marker -1 nor 10) radix has been specified.
+         * Accessor for checking whether non-default radix has been specified.
          *
          * @since 2.21
          */
         public boolean hasNonDefaultRadix() {
-            return _radix != DEFAULT_RADIX && _radix != 10;
+            return _radix != DEFAULT_RADIX;
         }
 
         /**
diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
index 642e06ce..b0ebce49 100644
--- a/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
+++ b/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
@@ -65,9 +65,9 @@ public void testEquality() {
 
     @Test
     public void testToString() {
-        assertEquals("JsonFormat.Value(pattern=,shape=STRING,lenient=null,locale=null,timezone=null,features=EMPTY,radix=10)",
+        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,radix=10)",
+        assertEquals("JsonFormat.Value(pattern=[.],shape=ANY,lenient=null,locale=null,timezone=null,features=EMPTY,radix=-1)",
                 JsonFormat.Value.forPattern("[.]").toString());
     }
 

From c5bcfa5c2eec70041602d3fc983019561179ea3b Mon Sep 17 00:00:00 2001
From: Davyd Fridman 
Date: Fri, 3 Oct 2025 10:34:47 -0500
Subject: [PATCH 4/4] Change type to int in JsonFormat radix test (#320)

---
 .../java/com/fasterxml/jackson/annotation/JsonFormatTest.java   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
index b0ebce49..ef5c4ce5 100644
--- a/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
+++ b/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java
@@ -266,7 +266,7 @@ public void testFeatures() {
     @Test
     void testRadix() {
         //Non-Default radix overrides the default
-        byte binaryRadix = 2;
+        int binaryRadix = 2;
         final JsonFormat.Value v = JsonFormat.Value.forRadix(binaryRadix);
         JsonFormat.Value merged = EMPTY.withOverrides(v);
         assertEquals(DEFAULT_RADIX, EMPTY.getRadix());