From 722182b5f0c5691e995bc185037ac37c3329df90 Mon Sep 17 00:00:00 2001 From: Davyd Fridman Date: Mon, 15 Sep 2025 20:52:57 -0500 Subject: [PATCH 1/7] Fix #221 Support alternate radixes for numeric values When writing numeric values as strings, we would like to support alternative to base-10 representations. Let's add it via @JsonFormat annotation. Whenever the shape is STRING and pattern is a number, we serialize/deserialize annotated properties using the specified alternative radix. --- .../databind/DeserializationConfig.java | 4 + .../jackson/databind/ObjectMapper.java | 14 +- .../jackson/databind/SerializationConfig.java | 4 + .../jackson/databind/cfg/BaseSettings.java | 89 ++++++--- .../jackson/databind/cfg/MapperConfig.java | 8 + .../databind/cfg/MapperConfigBase.java | 8 +- ...omStringWithRadixToNumberDeserializer.java | 50 +++++ .../deser/std/NumberDeserializers.java | 84 +++++++- .../databind/ser/std/NumberSerializer.java | 39 +++- .../databind/ser/std/NumberSerializers.java | 2 +- .../NumberToStringWithRadixSerializer.java | 64 ++++++ .../DifferentRadixNumberFormatTest.java | 187 ++++++++++++++++++ 12 files changed, 510 insertions(+), 43 deletions(-) create mode 100644 src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java create mode 100644 src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java create mode 100644 src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java index f32e2028efe..c264f63a44f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java @@ -373,6 +373,10 @@ public DeserializationConfig withRootName(PropertyName rootName) { return new DeserializationConfig(this, rootName); } + public DeserializationConfig withRadix(int radix) { + return _withBase(_base.withRadix(radix)); + } + @Override public DeserializationConfig withView(Class view) { return (_view == view) ? this : new DeserializationConfig(this, view); diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java index 102607f0d3f..f1369d31508 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java @@ -402,6 +402,7 @@ public boolean useForType(JavaType t) // 16-May-2009, tatu: Ditto ^^^ protected final static AnnotationIntrospector DEFAULT_ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector(); + protected static final int DEFAULT_RADIX = 10; /** * Base settings contain defaults used for all {@link ObjectMapper} * instances. @@ -419,7 +420,7 @@ public boolean useForType(JavaType t) // Since 2.12: new DefaultAccessorNamingStrategy.Provider(), // Since 2.16: [databind#2502] Add a way to configure Caches Jackson uses - DefaultCacheProvider.defaultInstance() + DefaultCacheProvider.defaultInstance(), DEFAULT_RADIX ); /* @@ -2545,6 +2546,17 @@ public ObjectMapper setDateFormat(DateFormat dateFormat) return this; } + /** + * Method for configuring the radix to use when serializing integral types + * as strings. + */ + public ObjectMapper setRadix(int radix) + { + _deserializationConfig = _deserializationConfig.withRadix(radix); + _serializationConfig = _serializationConfig.withRadix(radix); + return this; + } + /** * @since 2.5 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java b/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java index d12733629a5..67c2132e9ad 100644 --- a/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java @@ -371,6 +371,10 @@ public SerializationConfig with(SubtypeResolver str) { return (str == _subtypeResolver)? this : new SerializationConfig(this, str); } + public SerializationConfig withRadix(int radix) { + return _withBase(_base.withRadix(radix)); + } + @Override public SerializationConfig withView(Class view) { return (_view == view) ? this : new SerializationConfig(this, view); diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java index 76f2d9fe1a4..aacd70acb98 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java @@ -155,6 +155,11 @@ public final class BaseSettings */ protected final CacheProvider _cacheProvider; + /** + * Default radix to use when serializing/deserializing integers to string. + */ + protected final int _defaultRadix; + /* /********************************************************** /* Construction @@ -165,11 +170,11 @@ public final class BaseSettings * @since 2.19 */ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, - PropertyNamingStrategy pns, EnumNamingStrategy ens, TypeFactory tf, - TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, - Locale locale, TimeZone tz, Base64Variant defaultBase64, - PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, - CacheProvider cacheProvider) + PropertyNamingStrategy pns, EnumNamingStrategy ens, TypeFactory tf, + TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, + Locale locale, TimeZone tz, Base64Variant defaultBase64, + PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, + CacheProvider cacheProvider, int defaultRadix) { _classIntrospector = ci; _annotationIntrospector = ai; @@ -185,6 +190,7 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, _typeValidator = ptv; _accessorNaming = accNaming; _cacheProvider = cacheProvider; + _defaultRadix = defaultRadix; } /** @@ -193,13 +199,13 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, */ @Deprecated public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, - PropertyNamingStrategy pns, TypeFactory tf, - TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, - Locale locale, TimeZone tz, Base64Variant defaultBase64, - PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, - CacheProvider cacheProvider) + PropertyNamingStrategy pns, TypeFactory tf, + TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, + Locale locale, TimeZone tz, Base64Variant defaultBase64, + PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, + CacheProvider cacheProvider, int defaultRadix) { - this(ci, ai, pns, null, tf, typer, dateFormat, hi, locale, tz, defaultBase64, ptv, accNaming, cacheProvider); + this(ci, ai, pns, null, tf, typer, dateFormat, hi, locale, tz, defaultBase64, ptv, accNaming, cacheProvider, defaultRadix); } /** @@ -208,13 +214,13 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, */ @Deprecated public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, - PropertyNamingStrategy pns, TypeFactory tf, - TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, - Locale locale, TimeZone tz, Base64Variant defaultBase64, - PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming) + PropertyNamingStrategy pns, TypeFactory tf, + TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, + Locale locale, TimeZone tz, Base64Variant defaultBase64, + PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, int defaultRadix) { this(ci, ai, pns, tf, typer, dateFormat, hi, locale, tz, defaultBase64, ptv, accNaming, - DefaultCacheProvider.defaultInstance()); + DefaultCacheProvider.defaultInstance(), defaultRadix); } /** @@ -237,7 +243,8 @@ public BaseSettings copy() { _defaultBase64, _typeValidator, _accessorNaming, - _cacheProvider); + _cacheProvider, + _defaultRadix); } /* @@ -252,7 +259,7 @@ public BaseSettings withClassIntrospector(ClassIntrospector ci) { } return new BaseSettings(ci, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale, - _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider); + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } public BaseSettings withAnnotationIntrospector(AnnotationIntrospector ai) { @@ -261,7 +268,7 @@ public BaseSettings withAnnotationIntrospector(AnnotationIntrospector ai) { } return new BaseSettings(_classIntrospector, ai, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale, - _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider); + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } public BaseSettings withInsertedAnnotationIntrospector(AnnotationIntrospector ai) { @@ -288,7 +295,7 @@ public BaseSettings withPropertyNamingStrategy(PropertyNamingStrategy pns) { } return new BaseSettings(_classIntrospector, _annotationIntrospector, pns, _enumNamingStrategy, _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale, - _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider); + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } /** @@ -300,7 +307,7 @@ public BaseSettings withEnumNamingStrategy(EnumNamingStrategy ens) { } return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, ens, _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale, - _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider); + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } // @since 2.12 @@ -310,7 +317,7 @@ public BaseSettings withAccessorNaming(AccessorNamingStrategy.Provider p) { } return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale, - _timeZone, _defaultBase64, _typeValidator, p, _cacheProvider); + _timeZone, _defaultBase64, _typeValidator, p, _cacheProvider, _defaultRadix); } public BaseSettings withTypeFactory(TypeFactory tf) { @@ -319,7 +326,7 @@ public BaseSettings withTypeFactory(TypeFactory tf) { } return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, tf, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale, - _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider); + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } public BaseSettings withTypeResolverBuilder(TypeResolverBuilder typer) { @@ -328,7 +335,7 @@ public BaseSettings withTypeResolverBuilder(TypeResolverBuilder typer) { } return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, typer, _dateFormat, _handlerInstantiator, _locale, - _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider); + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } public BaseSettings withDateFormat(DateFormat df) { @@ -342,7 +349,7 @@ public BaseSettings withDateFormat(DateFormat df) { } return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, _typeResolverBuilder, df, _handlerInstantiator, _locale, - _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider); + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } public BaseSettings withHandlerInstantiator(HandlerInstantiator hi) { @@ -351,7 +358,7 @@ public BaseSettings withHandlerInstantiator(HandlerInstantiator hi) { } return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, _typeResolverBuilder, _dateFormat, hi, _locale, - _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider); + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } public BaseSettings with(Locale l) { @@ -360,7 +367,7 @@ public BaseSettings with(Locale l) { } return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory,_typeResolverBuilder, _dateFormat, _handlerInstantiator, l, - _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider); + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } /** @@ -383,7 +390,7 @@ public BaseSettings with(TimeZone tz) return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, _typeResolverBuilder, df, _handlerInstantiator, _locale, - tz, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider); + tz, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } /** @@ -396,7 +403,7 @@ public BaseSettings with(Base64Variant base64) { return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale, - _timeZone, base64, _typeValidator, _accessorNaming, _cacheProvider); + _timeZone, base64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix); } /** @@ -409,7 +416,7 @@ public BaseSettings with(PolymorphicTypeValidator v) { return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale, - _timeZone, _defaultBase64, v, _accessorNaming, _cacheProvider); + _timeZone, _defaultBase64, v, _accessorNaming, _cacheProvider, _defaultRadix); } /** @@ -425,7 +432,23 @@ public BaseSettings with(CacheProvider cacheProvider) { return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale, - _timeZone, _defaultBase64, _typeValidator, _accessorNaming, cacheProvider); + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, cacheProvider, _defaultRadix); + } + + /** + * Fluent factory for constructing a new instance with provided default radix. + * + * @return a new instance with provided defaultRadix. + * @since 2.21 + */ + public BaseSettings withRadix(int defaultRadix) { + if (defaultRadix == _defaultRadix) { + return this; + } + return new BaseSettings(_classIntrospector, _annotationIntrospector, + _propertyNamingStrategy, _enumNamingStrategy, _typeFactory, + _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale, + _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, defaultRadix); } /* @@ -503,6 +526,10 @@ public boolean hasExplicitTimeZone() { public Base64Variant getBase64Variant() { return _defaultBase64; } + + public int getRadix() { + return _defaultRadix; + } /** * @since 2.16 diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java index 061224e1e5f..73af5779521 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java @@ -671,6 +671,14 @@ public Base64Variant getBase64Variant() { return _base.getBase64Variant(); } + /** + * Method called during deserialization if a Number needs to be decoded from + * String. Default version just returns radix 10. + */ + public int getRadix() { + return _base.getRadix(); + } + /** * Method for accessing per-instance shared (baseline/default) * attribute values; these are used as the basis for per-call diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java index 139253571a7..a294842d1db 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java @@ -682,6 +682,12 @@ public T withRootName(String rootName) { */ public abstract T with(SubtypeResolver str); + /** + * Method for constructing and returning a new instance with different + * radix to use. + */ + public abstract T withRadix(int radix); + /** * Method for constructing and returning a new instance with different * view to use. @@ -933,8 +939,8 @@ public PropertyName findRootName(Class rawRootType) { public final Class findMixInClassFor(Class cls) { return _mixIns.findMixInClassFor(cls); } - // Not really relevant here (should not get called) + @Override public MixInResolver copy() { throw new UnsupportedOperationException(); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java new file mode 100644 index 00000000000..a179ea0428c --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java @@ -0,0 +1,50 @@ +package com.fasterxml.jackson.databind.deser.std; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; + +import java.io.IOException; +import java.math.BigInteger; + +/** + * Deserializer used to deserialize string that represent number under in specific radix (base). + */ +public class FromStringWithRadixToNumberDeserializer + extends StdDeserializer { + private final int radix; + + public FromStringWithRadixToNumberDeserializer(StdDeserializer src, int radix) { + super(src); + this.radix = radix; + } + + @Override + public Number deserialize(JsonParser p, DeserializationContext ctxt) + throws IOException { + Class handledType = handledType(); + + if (p.currentToken() != JsonToken.VALUE_STRING) { + ctxt.reportInputMismatch(handledType, + "Read something other than string when deserializing a value using FromStringWithRadixToNumberDeserializer."); + } + + String text = p.getText(); + + if (handledType.equals(BigInteger.class)) { + return new BigInteger(text, radix); + } else if (handledType.equals(byte.class) || handledType.equals(Byte.class)) { + return Byte.parseByte(text, radix); + } else if (handledType.equals(short.class) || handledType.equals(Short.class)) { + return Short.parseShort(text, radix); + } else if (handledType.equals(int.class) || handledType.equals(Integer.class)) { + return Integer.parseInt(text, radix); + } else if (handledType.equals(long.class) || handledType.equals(Long.class)) { + return Long.parseLong(text, radix); + } else { + ctxt.reportInputMismatch(handledType, + "Trying to deserialize a non-whole number with NumberToStringWithRadixSerializer"); + return null;//should not reach here + } + } +} diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java index 029e36e1ef5..4994dc8ce70 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java @@ -5,12 +5,14 @@ import java.math.BigInteger; import java.util.HashSet; +import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.core.*; import com.fasterxml.jackson.core.io.NumberInput; import com.fasterxml.jackson.databind.*; import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; import com.fasterxml.jackson.databind.cfg.CoercionAction; import com.fasterxml.jackson.databind.cfg.CoercionInputShape; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; @@ -26,6 +28,8 @@ public class NumberDeserializers { private final static HashSet _classNames = new HashSet(); + private final static int DEFAULT_RADIX = 10; + static { // note: can skip primitive types; other ways to check them: Class[] numberTypes = new Class[] { @@ -250,7 +254,7 @@ public Boolean deserializeWithType(JsonParser p, DeserializationContext ctxt, @JacksonStdImpl public static class ByteDeserializer - extends PrimitiveOrWrapperDeserializer + extends PrimitiveOrWrapperDeserializer implements ContextualDeserializer { private static final long serialVersionUID = 1L; @@ -274,6 +278,12 @@ public Byte deserialize(JsonParser p, DeserializationContext ctxt) throws IOExce return _parseByte(p, ctxt); } + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { + return _createRadixStringDeserializer(this, ctxt, property); + } + protected Byte _parseByte(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -342,7 +352,7 @@ protected Byte _parseByte(JsonParser p, DeserializationContext ctxt) @JacksonStdImpl public static class ShortDeserializer - extends PrimitiveOrWrapperDeserializer + extends PrimitiveOrWrapperDeserializer implements ContextualDeserializer { private static final long serialVersionUID = 1L; @@ -367,6 +377,12 @@ public Short deserialize(JsonParser p, DeserializationContext ctxt) return _parseShort(p, ctxt); } + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { + return _createRadixStringDeserializer(this, ctxt, property); + } + protected Short _parseShort(JsonParser p, DeserializationContext ctxt) throws IOException { @@ -517,8 +533,7 @@ public Character deserialize(JsonParser p, DeserializationContext ctxt) @JacksonStdImpl public final static class IntegerDeserializer - extends PrimitiveOrWrapperDeserializer - { + extends PrimitiveOrWrapperDeserializer implements ContextualDeserializer { private static final long serialVersionUID = 1L; final static IntegerDeserializer primitiveInstance = new IntegerDeserializer(Integer.TYPE, 0); @@ -557,11 +572,17 @@ public Integer deserializeWithType(JsonParser p, DeserializationContext ctxt, } return _parseInteger(p, ctxt, Integer.class); } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { + return _createRadixStringDeserializer(this, ctxt, property); + } } @JacksonStdImpl public final static class LongDeserializer - extends PrimitiveOrWrapperDeserializer + extends PrimitiveOrWrapperDeserializer implements ContextualDeserializer { private static final long serialVersionUID = 1L; @@ -586,6 +607,12 @@ public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOExce } return _parseLong(p, ctxt, Long.class); } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { + return _createRadixStringDeserializer(this, ctxt, property); + } } @JacksonStdImpl @@ -936,7 +963,7 @@ public Object deserializeWithType(JsonParser p, DeserializationContext ctxt, @SuppressWarnings("serial") @JacksonStdImpl public static class BigIntegerDeserializer - extends StdScalarDeserializer + extends StdScalarDeserializer implements ContextualDeserializer { public final static BigIntegerDeserializer instance = new BigIntegerDeserializer(); @@ -1011,6 +1038,12 @@ public BigInteger deserialize(JsonParser p, DeserializationContext ctxt) throws return (BigInteger) ctxt.handleWeirdStringValue(_valueClass, text, "not a valid representation"); } + + @Override + public JsonDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) + throws JsonMappingException { + return _createRadixStringDeserializer(this, ctxt, property); + } } @SuppressWarnings("serial") @@ -1089,4 +1122,43 @@ public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt) "not a valid representation"); } } + + private static StdDeserializer _createRadixStringDeserializer(StdScalarDeserializer initialDeser, + DeserializationContext ctxt, BeanProperty property) + { + JsonFormat.Value format = initialDeser.findFormatOverrides(ctxt, property, initialDeser.handledType()); + + if (format == null) { + return initialDeser; + } + + if (isSerializeWithRadixOverride(format)) { + int radix = Integer.parseInt(format.getPattern()); + return new FromStringWithRadixToNumberDeserializer(initialDeser, radix); + } else if (isSerializeWithDefaultOverride(ctxt)) { + int radix = ctxt.getConfig().getRadix(); + return new FromStringWithRadixToNumberDeserializer(initialDeser, radix); + } + + return initialDeser; + } + + + private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) { + String pattern = format.getPattern(); + boolean isInteger = pattern.chars().allMatch(Character::isDigit); + if (!isInteger || pattern.isEmpty()) { + return false; + } + + int radix = Integer.parseInt(pattern); + return radix != DEFAULT_RADIX; + } + + private static boolean isSerializeWithDefaultOverride(DeserializationContext ctxt) { + if (ctxt.getConfig() == null) { + return false; + } + return ctxt.getConfig().getRadix() != DEFAULT_RADIX; + } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java index 56c2391daea..6fdb773ee6b 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java @@ -37,6 +37,8 @@ public class NumberSerializer protected final boolean _isInt; + protected static final int DEFAULT_RADIX = 10; + /** * @since 2.5 */ @@ -50,7 +52,7 @@ public NumberSerializer(Class rawType) { public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { - JsonFormat.Value format = findFormatOverrides(prov, property, handledType()); + JsonFormat.Value format = findFormatOverrides(prov, property, handledType());//this handles both the annotation and ConfigOverrides case. if (format != null) { switch (format.getShape()) { case STRING: @@ -58,8 +60,8 @@ public JsonSerializer createContextual(SerializerProvider prov, if (((Class) handledType()) == BigDecimal.class) { return bigDecimalAsStringSerializer(); } - return ToStringSerializer.instance; - default: + return createStringSerializer(prov, format, _isInt); + default: } } return this; @@ -114,6 +116,37 @@ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType t } } + public static ToStringSerializerBase createStringSerializer(SerializerProvider prov, JsonFormat.Value format, boolean isInt) { + if (isInt) { + if (isSerializeWithRadixOverride(format)) { + int radix = Integer.parseInt(format.getPattern()); + return new NumberToStringWithRadixSerializer(radix); + } else if (isSerializeWithDefaultOverride(prov)) { + int radix = prov.getConfig().getRadix(); + return new NumberToStringWithRadixSerializer(radix); + } + } + return ToStringSerializer.instance; + } + + private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) { + String pattern = format.getPattern(); + boolean isInteger = pattern.chars().allMatch(Character::isDigit); + if (!isInteger || pattern.isEmpty()) { + return false; + } + + int radix = Integer.parseInt(pattern); + return radix != DEFAULT_RADIX; + } + + private static boolean isSerializeWithDefaultOverride(SerializerProvider prov) { + if (prov.getConfig() == null) { + return false; + } + return prov.getConfig().getRadix() != DEFAULT_RADIX; + } + /** * @since 2.10 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializers.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializers.java index be7e956a2fc..f5a36006a98 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializers.java @@ -106,7 +106,7 @@ public JsonSerializer createContextual(SerializerProvider prov, if (((Class) handledType()) == BigDecimal.class) { return NumberSerializer.bigDecimalAsStringSerializer(); } - return ToStringSerializer.instance; + return NumberSerializer.createStringSerializer(prov, format, _isInt); default: } } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java new file mode 100644 index 00000000000..6639c19404a --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java @@ -0,0 +1,64 @@ +package com.fasterxml.jackson.databind.ser.std; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JacksonStdImpl; + +import java.io.IOException; +import java.math.BigInteger; + +/** + * Serializer used to convert numbers into a representation for a specified radix (base) and serialize + * the representation as string. + */ +@JacksonStdImpl +public class NumberToStringWithRadixSerializer extends ToStringSerializerBase { + private final int radix; + + public NumberToStringWithRadixSerializer(int radix) { super(Object.class); + this.radix = radix; + } + + public NumberToStringWithRadixSerializer(Class handledType, int radix) { + super(handledType); + this.radix = radix; + } + + @Override + public boolean isEmpty(SerializerProvider prov, Object value) { + return false; + } + + @Override + public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + if (radix < Character.MIN_RADIX || radix > Character.MAX_RADIX) { + String errorMsg = String.format("To use a custom radix for string serialization, use radix within [%d, %d]", Character.MIN_RADIX, Character.MAX_RADIX); + provider.reportBadDefinition(handledType(), errorMsg); + } + + String text = ""; + if (value instanceof BigInteger bigInt) { + text = bigInt.toString(radix); + } else if (value instanceof Byte + || value instanceof Short + || value instanceof Integer + || value instanceof Long) { + long longValue = ((Number) value).longValue(); + text = Long.toString(longValue, radix); + } else { + provider.reportBadDefinition(handledType(), + "Trying to serialize a non-whole number with NumberToStringWithRadixSerializer"); + } + + gen.writeString(text); + + } + + @Override + public String valueToString(Object value) { + // should never be called + throw new IllegalStateException(); + } +} diff --git a/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java b/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java new file mode 100644 index 00000000000..6ea5a8ddd12 --- /dev/null +++ b/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java @@ -0,0 +1,187 @@ +package com.fasterxml.jackson.databind.format; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.testutil.DatabindTestUtil; +import org.junit.jupiter.api.Test; + +import java.math.BigInteger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class DifferentRadixNumberFormatTest extends DatabindTestUtil { + + private static class IntegerWrapper { + public Integer value; + + public IntegerWrapper() {} + public IntegerWrapper(Integer v) { value = v; } + } + + private static class IntWrapper { + public int value; + + public IntWrapper() {} + public IntWrapper(int v) { value = v; } + } + + private static class AnnotatedMethodIntWrapper { + private int value; + + public AnnotatedMethodIntWrapper() { + } + public AnnotatedMethodIntWrapper(int v) { + value = v; + } + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "16") + public int getValue() { + return value; + } + } + + private static class AllIntegralTypeWrapper { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2") + public byte byteValue; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2") + public Byte ByteValue; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2") + public short shortValue; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2") + public Short ShortValue; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2") + public int intValue; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2") + public Integer IntegerValue; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2") + public long longValue; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2") + public Long LongValue; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2") + public BigInteger bigInteger; + + public AllIntegralTypeWrapper() { + } + + public AllIntegralTypeWrapper(byte byteValue, Byte ByteValue, short shortValue, Short ShortValue, int intValue, + Integer IntegerValue, long longValue, Long LongValue, BigInteger bigInteger) { + this.byteValue = byteValue; + this.ByteValue = ByteValue; + this.shortValue = shortValue; + this.ShortValue = ShortValue; + this.intValue = intValue; + this.IntegerValue = IntegerValue; + this.longValue = longValue; + this.LongValue = LongValue; + this.bigInteger = bigInteger; + } + } + + @Test + void testIntegerSerializedAsHexString() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + mapper.configOverride(Integer.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING).withPattern("16")); + IntegerWrapper initialIntegerWrapper = new IntegerWrapper(10); + String json = mapper.writeValueAsString(initialIntegerWrapper); + String expectedJson = "{'value':'a'}"; + + assertEquals(a2q(expectedJson), json); + + IntegerWrapper readBackIntegerWrapper = mapper.readValue(a2q(expectedJson), IntegerWrapper.class); + + assertNotNull(readBackIntegerWrapper); + assertEquals(initialIntegerWrapper.value, readBackIntegerWrapper.value); + } + + + @Test + void testIntSerializedAsHexString() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + mapper.configOverride(int.class) + .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING).withPattern("16")); + IntWrapper intialIntWrapper = new IntWrapper(10); + String expectedJson = "{'value':'a'}"; + + String json = mapper.writeValueAsString(intialIntWrapper); + + assertEquals(a2q(expectedJson), json); + + IntWrapper readBackIntWrapper = mapper.readValue(a2q(expectedJson), IntWrapper.class); + + assertNotNull(readBackIntWrapper); + assertEquals(intialIntWrapper.value, readBackIntWrapper.value); + + } + + @Test + void testAnnotatedAccessorSerializedAsHexString() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + AnnotatedMethodIntWrapper initialIntWrapper = new AnnotatedMethodIntWrapper(10); + String expectedJson = "{'value':'a'}"; + + String json = mapper.writeValueAsString(initialIntWrapper); + + assertEquals(a2q(expectedJson), json); + + AnnotatedMethodIntWrapper readBackIntWrapper = mapper.readValue(a2q(expectedJson), AnnotatedMethodIntWrapper.class); + + assertNotNull(readBackIntWrapper); + assertEquals(initialIntWrapper.value, readBackIntWrapper.value); + } + + @Test + void testUsingBaseSettingRadixToSerializeAsHexString() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + mapper.configOverride(Integer.class) + .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); + mapper.setRadix(16); + IntegerWrapper intialIntegerWrapper = new IntegerWrapper(10); + String expectedJson = "{'value':'a'}"; + + String json = mapper.writeValueAsString(intialIntegerWrapper); + + assertEquals(a2q(expectedJson), json); + + IntegerWrapper readBackIntegerWrapper = mapper.readValue(a2q(expectedJson), IntegerWrapper.class); + + assertNotNull(readBackIntegerWrapper); + assertEquals(intialIntegerWrapper.value, readBackIntegerWrapper.value); + } + + @Test + void testAllIntegralTypesGetSerializedWithRadix() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + mapper.configOverride(Integer.class) + .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); + mapper.setRadix(16); + AllIntegralTypeWrapper intialIntegralTypeWrapper = new AllIntegralTypeWrapper((byte) 1, + (byte) 2, (short) 3, (short) 4, 5, 6, 7L, 8L, new BigInteger("9")); + String expectedJson = "{'byteValue':'1','ByteValue':'10','shortValue':'11','ShortValue':'100','intValue':'101','IntegerValue':'110','longValue':'111','LongValue':'1000','bigInteger':'1001'}"; + + String json = mapper.writeValueAsString(intialIntegralTypeWrapper); + + AllIntegralTypeWrapper readbackIntegralTypeWrapper = mapper.readValue(a2q(expectedJson), AllIntegralTypeWrapper.class); + + assertNotNull(readbackIntegralTypeWrapper); + assertEquals(intialIntegralTypeWrapper.byteValue, readbackIntegralTypeWrapper.byteValue); + assertEquals(intialIntegralTypeWrapper.ByteValue, readbackIntegralTypeWrapper.ByteValue); + assertEquals(intialIntegralTypeWrapper.shortValue, readbackIntegralTypeWrapper.shortValue); + assertEquals(intialIntegralTypeWrapper.ShortValue, readbackIntegralTypeWrapper.ShortValue); + assertEquals(intialIntegralTypeWrapper.intValue, readbackIntegralTypeWrapper.intValue); + assertEquals(intialIntegralTypeWrapper.IntegerValue, readbackIntegralTypeWrapper.IntegerValue); + assertEquals(intialIntegralTypeWrapper.longValue, readbackIntegralTypeWrapper.longValue); + assertEquals(intialIntegralTypeWrapper.LongValue, readbackIntegralTypeWrapper.LongValue); + assertEquals(intialIntegralTypeWrapper.bigInteger, readbackIntegralTypeWrapper.bigInteger); + } +} From 6096e278f501b21bc8cfbed3067e83ad89a582c3 Mon Sep 17 00:00:00 2001 From: Davyd Fridman Date: Mon, 15 Sep 2025 21:34:45 -0500 Subject: [PATCH 2/7] Fix #221 - address PR comments --- .../jackson/databind/ObjectMapper.java | 2 + .../jackson/databind/cfg/BaseSettings.java | 43 ++++++++++++++++--- .../jackson/databind/cfg/MapperConfig.java | 2 + .../databind/cfg/MapperConfigBase.java | 2 + ...omStringWithRadixToNumberDeserializer.java | 2 + .../databind/ser/std/NumberSerializer.java | 3 +- .../NumberToStringWithRadixSerializer.java | 2 + 7 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java index f1369d31508..20035deb0f7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java @@ -2549,6 +2549,8 @@ public ObjectMapper setDateFormat(DateFormat dateFormat) /** * Method for configuring the radix to use when serializing integral types * as strings. + * + * @since 2.21 */ public ObjectMapper setRadix(int radix) { diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java index aacd70acb98..94f5f121c0e 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java @@ -35,6 +35,8 @@ public final class BaseSettings */ TimeZone.getTimeZone("UTC"); + private static final int DEFAULT_RADIX = 10; + /* /********************************************************** /* Configuration settings; introspection, related @@ -167,7 +169,7 @@ public final class BaseSettings */ /** - * @since 2.19 + * @since 2.21 */ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, PropertyNamingStrategy pns, EnumNamingStrategy ens, TypeFactory tf, @@ -193,6 +195,34 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, _defaultRadix = defaultRadix; } + /** + * @since 2.19 + * @deprecated since 2.21, use variant that take defaultRadix + */ + public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, + PropertyNamingStrategy pns, EnumNamingStrategy ens, TypeFactory tf, + TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, + Locale locale, TimeZone tz, Base64Variant defaultBase64, + PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, + CacheProvider cacheProvider) + { + this(ci, + ai, + pns, + ens, + tf, + typer, + dateFormat, + hi, + locale, + tz, + defaultBase64, + ptv, + accNaming, + cacheProvider, + DEFAULT_RADIX); + } + /** * @since 2.16 * @deprecated Since 2.19, use variant that takes {@link EnumNamingStrategy} instead. @@ -203,9 +233,9 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, Locale locale, TimeZone tz, Base64Variant defaultBase64, PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, - CacheProvider cacheProvider, int defaultRadix) + CacheProvider cacheProvider) { - this(ci, ai, pns, null, tf, typer, dateFormat, hi, locale, tz, defaultBase64, ptv, accNaming, cacheProvider, defaultRadix); + this(ci, ai, pns, null, tf, typer, dateFormat, hi, locale, tz, defaultBase64, ptv, accNaming, cacheProvider); } /** @@ -217,10 +247,10 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, PropertyNamingStrategy pns, TypeFactory tf, TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, Locale locale, TimeZone tz, Base64Variant defaultBase64, - PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, int defaultRadix) + PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming) { this(ci, ai, pns, tf, typer, dateFormat, hi, locale, tz, defaultBase64, ptv, accNaming, - DefaultCacheProvider.defaultInstance(), defaultRadix); + DefaultCacheProvider.defaultInstance()); } /** @@ -527,6 +557,9 @@ public Base64Variant getBase64Variant() { return _defaultBase64; } + /** + * @since 2.21 + */ public int getRadix() { return _defaultRadix; } diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java index 73af5779521..1867d6ce30f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java @@ -674,6 +674,8 @@ public Base64Variant getBase64Variant() { /** * Method called during deserialization if a Number needs to be decoded from * String. Default version just returns radix 10. + * + * @since 2.21 */ public int getRadix() { return _base.getRadix(); diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java index a294842d1db..725f329b0d0 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java @@ -685,6 +685,8 @@ public T withRootName(String rootName) { /** * Method for constructing and returning a new instance with different * radix to use. + * + * @since 2.21 */ public abstract T withRadix(int radix); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java index a179ea0428c..0324ed98dfb 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java @@ -9,6 +9,8 @@ /** * Deserializer used to deserialize string that represent number under in specific radix (base). + * + * @since 2.21 */ public class FromStringWithRadixToNumberDeserializer extends StdDeserializer { diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java index 6fdb773ee6b..69dd4c2da37 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java @@ -61,7 +61,8 @@ public JsonSerializer createContextual(SerializerProvider prov, return bigDecimalAsStringSerializer(); } return createStringSerializer(prov, format, _isInt); - default: + + default: } } return this; diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java index 6639c19404a..29f79a71c3f 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java @@ -10,6 +10,8 @@ /** * Serializer used to convert numbers into a representation for a specified radix (base) and serialize * the representation as string. + * + * @since 2.21 */ @JacksonStdImpl public class NumberToStringWithRadixSerializer extends ToStringSerializerBase { From 7a211dbef9523ceb7d062839e8dcb8ca084a4d2b Mon Sep 17 00:00:00 2001 From: Davyd Fridman Date: Tue, 16 Sep 2025 14:50:40 -0500 Subject: [PATCH 3/7] Fix #221 - address PR comments --- .../databind/DeserializationConfig.java | 1 + .../jackson/databind/ObjectMapper.java | 4 +- .../jackson/databind/SerializationConfig.java | 1 + .../jackson/databind/cfg/BaseSettings.java | 50 ++++++++----------- .../jackson/databind/cfg/MapperConfig.java | 3 +- .../databind/cfg/MapperConfigBase.java | 2 +- ...omStringWithRadixToNumberDeserializer.java | 2 +- .../deser/std/NumberDeserializers.java | 24 +++++++-- .../databind/ser/std/NumberSerializer.java | 20 ++++++-- .../DifferentRadixNumberFormatTest.java | 29 ++++++----- 10 files changed, 78 insertions(+), 58 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java index c264f63a44f..56da80064b4 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java @@ -373,6 +373,7 @@ public DeserializationConfig withRootName(PropertyName rootName) { return new DeserializationConfig(this, rootName); } + @Override public DeserializationConfig withRadix(int radix) { return _withBase(_base.withRadix(radix)); } diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java index 20035deb0f7..acbfe9aaa93 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java @@ -420,7 +420,9 @@ public boolean useForType(JavaType t) // Since 2.12: new DefaultAccessorNamingStrategy.Provider(), // Since 2.16: [databind#2502] Add a way to configure Caches Jackson uses - DefaultCacheProvider.defaultInstance(), DEFAULT_RADIX + DefaultCacheProvider.defaultInstance(), + //since 2.21: [databind#221] - support alternate radixes for numerical values serialized as strings + DEFAULT_RADIX ); /* diff --git a/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java b/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java index 67c2132e9ad..ec3737f79b1 100644 --- a/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java @@ -371,6 +371,7 @@ public SerializationConfig with(SubtypeResolver str) { return (str == _subtypeResolver)? this : new SerializationConfig(this, str); } + @Override public SerializationConfig withRadix(int radix) { return _withBase(_base.withRadix(radix)); } diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java index 94f5f121c0e..56c523d56c5 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java @@ -159,6 +159,8 @@ public final class BaseSettings /** * Default radix to use when serializing/deserializing integers to string. + * + * @since 2.21 */ protected final int _defaultRadix; @@ -197,30 +199,16 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, /** * @since 2.19 - * @deprecated since 2.21, use variant that take defaultRadix + * @deprecated since 2.21, use variant that takes defaultRadix */ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, - PropertyNamingStrategy pns, EnumNamingStrategy ens, TypeFactory tf, - TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, - Locale locale, TimeZone tz, Base64Variant defaultBase64, - PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, - CacheProvider cacheProvider) + PropertyNamingStrategy pns, EnumNamingStrategy ens, TypeFactory tf, + TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, + Locale locale, TimeZone tz, Base64Variant defaultBase64, + PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, + CacheProvider cacheProvider) { - this(ci, - ai, - pns, - ens, - tf, - typer, - dateFormat, - hi, - locale, - tz, - defaultBase64, - ptv, - accNaming, - cacheProvider, - DEFAULT_RADIX); + this(ci, ai, pns, ens, tf, typer, dateFormat, hi, locale, tz, defaultBase64, ptv, accNaming, cacheProvider, DEFAULT_RADIX); } /** @@ -229,11 +217,11 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, */ @Deprecated public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, - PropertyNamingStrategy pns, TypeFactory tf, - TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, - Locale locale, TimeZone tz, Base64Variant defaultBase64, - PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, - CacheProvider cacheProvider) + PropertyNamingStrategy pns, TypeFactory tf, + TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, + Locale locale, TimeZone tz, Base64Variant defaultBase64, + PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, + CacheProvider cacheProvider) { this(ci, ai, pns, null, tf, typer, dateFormat, hi, locale, tz, defaultBase64, ptv, accNaming, cacheProvider); } @@ -244,10 +232,10 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, */ @Deprecated public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, - PropertyNamingStrategy pns, TypeFactory tf, - TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, - Locale locale, TimeZone tz, Base64Variant defaultBase64, - PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming) + PropertyNamingStrategy pns, TypeFactory tf, + TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, + Locale locale, TimeZone tz, Base64Variant defaultBase64, + PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming) { this(ci, ai, pns, tf, typer, dateFormat, hi, locale, tz, defaultBase64, ptv, accNaming, DefaultCacheProvider.defaultInstance()); @@ -558,6 +546,8 @@ public Base64Variant getBase64Variant() { } /** + * Method indicating number base to use for serializing/deserializing an integral number as a string. + * * @since 2.21 */ public int getRadix() { diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java index 1867d6ce30f..0bf2b5fb667 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java @@ -672,8 +672,7 @@ public Base64Variant getBase64Variant() { } /** - * Method called during deserialization if a Number needs to be decoded from - * String. Default version just returns radix 10. + * Method indicating which number base to use for serializing/deserializing an integral number as a string. * * @since 2.21 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java index 725f329b0d0..4a13626ca95 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java @@ -941,8 +941,8 @@ public PropertyName findRootName(Class rawRootType) { public final Class findMixInClassFor(Class cls) { return _mixIns.findMixInClassFor(cls); } - // Not really relevant here (should not get called) + // Not really relevant here (should not get called) @Override public MixInResolver copy() { throw new UnsupportedOperationException(); diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java index 0324ed98dfb..7f50d225c49 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java @@ -8,7 +8,7 @@ import java.math.BigInteger; /** - * Deserializer used to deserialize string that represent number under in specific radix (base). + * Deserializer used ii a string that represents a number in specific radix (base). * * @since 2.21 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java index 4994dc8ce70..815282fb817 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.cfg.CoercionInputShape; import com.fasterxml.jackson.databind.deser.ContextualDeserializer; import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import com.fasterxml.jackson.databind.ser.std.NumberToStringWithRadixSerializer; import com.fasterxml.jackson.databind.type.LogicalType; import com.fasterxml.jackson.databind.util.AccessPattern; import com.fasterxml.jackson.databind.util.ClassUtil; @@ -1123,19 +1124,25 @@ public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt) } } + /** + * Method used to create a string deserializer for a Number. + * If configuration is set properly, we create an alternative radix serializer {@link NumberToStringWithRadixSerializer}. + * + * @since 2.21 + */ private static StdDeserializer _createRadixStringDeserializer(StdScalarDeserializer initialDeser, - DeserializationContext ctxt, BeanProperty property) + DeserializationContext ctxt, BeanProperty property) { JsonFormat.Value format = initialDeser.findFormatOverrides(ctxt, property, initialDeser.handledType()); - if (format == null) { + if (format == null || format.getShape() != JsonFormat.Shape.STRING) { return initialDeser; } if (isSerializeWithRadixOverride(format)) { int radix = Integer.parseInt(format.getPattern()); return new FromStringWithRadixToNumberDeserializer(initialDeser, radix); - } else if (isSerializeWithDefaultOverride(ctxt)) { + } else if (isSerializeWithDefaultConfigOverride(ctxt)) { int radix = ctxt.getConfig().getRadix(); return new FromStringWithRadixToNumberDeserializer(initialDeser, radix); } @@ -1143,7 +1150,10 @@ private static StdDeserializer _createRadixStringDeserializer( return initialDeser; } - + /** + * Check if we have a proper {@link JsonFormat} annotation for serializing a Number + * using an alternative radix specified in the annotation. + */ private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) { String pattern = format.getPattern(); boolean isInteger = pattern.chars().allMatch(Character::isDigit); @@ -1155,7 +1165,11 @@ private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) { return radix != DEFAULT_RADIX; } - private static boolean isSerializeWithDefaultOverride(DeserializationContext ctxt) { + /** + * Check if we have a non-default radix specified as part of {@link com.fasterxml.jackson.databind.cfg.BaseSettings} contained + * in {@link DeserializationConfig}. + */ + private static boolean isSerializeWithDefaultConfigOverride(DeserializationContext ctxt) { if (ctxt.getConfig() == null) { return false; } diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java index 69dd4c2da37..5a3fbf08ed4 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java @@ -52,7 +52,7 @@ public NumberSerializer(Class rawType) { public JsonSerializer createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException { - JsonFormat.Value format = findFormatOverrides(prov, property, handledType());//this handles both the annotation and ConfigOverrides case. + JsonFormat.Value format = findFormatOverrides(prov, property, handledType()); if (format != null) { switch (format.getShape()) { case STRING: @@ -117,12 +117,18 @@ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType t } } + /** + * Method used to create a string serializer for a Number. If the number is an integer, and configuration is set properly, + * we create an alternative radix serializer {@link NumberToStringWithRadixSerializer}. + * + * @since 2.21 + */ public static ToStringSerializerBase createStringSerializer(SerializerProvider prov, JsonFormat.Value format, boolean isInt) { if (isInt) { if (isSerializeWithRadixOverride(format)) { int radix = Integer.parseInt(format.getPattern()); return new NumberToStringWithRadixSerializer(radix); - } else if (isSerializeWithDefaultOverride(prov)) { + } else if (isSerializeWithDefaultConfigOverride(prov)) { int radix = prov.getConfig().getRadix(); return new NumberToStringWithRadixSerializer(radix); } @@ -130,6 +136,10 @@ public static ToStringSerializerBase createStringSerializer(SerializerProvider p return ToStringSerializer.instance; } + /** + * Check if we have a proper {@link JsonFormat} annotation for serializing a Number + * using an alternative radix specified in the annotation. + */ private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) { String pattern = format.getPattern(); boolean isInteger = pattern.chars().allMatch(Character::isDigit); @@ -141,7 +151,11 @@ private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) { return radix != DEFAULT_RADIX; } - private static boolean isSerializeWithDefaultOverride(SerializerProvider prov) { + /** + * Check if we have a non-default radix specified as part of {@link com.fasterxml.jackson.databind.cfg.BaseSettings} contained + * in {@link SerializationConfig}. + */ + private static boolean isSerializeWithDefaultConfigOverride(SerializerProvider prov) { if (prov.getConfig() == null) { return false; } diff --git a/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java b/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java index 6ea5a8ddd12..6a48f2b2e03 100644 --- a/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java @@ -159,29 +159,28 @@ void testUsingBaseSettingRadixToSerializeAsHexString() } @Test - void testAllIntegralTypesGetSerializedWithRadix() + void testAllIntegralTypesGetSerializedAsBinary() throws JsonProcessingException { ObjectMapper mapper = newJsonMapper(); - mapper.configOverride(Integer.class) - .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING)); - mapper.setRadix(16); - AllIntegralTypeWrapper intialIntegralTypeWrapper = new AllIntegralTypeWrapper((byte) 1, + AllIntegralTypeWrapper initialIntegralTypeWrapper = new AllIntegralTypeWrapper((byte) 1, (byte) 2, (short) 3, (short) 4, 5, 6, 7L, 8L, new BigInteger("9")); String expectedJson = "{'byteValue':'1','ByteValue':'10','shortValue':'11','ShortValue':'100','intValue':'101','IntegerValue':'110','longValue':'111','LongValue':'1000','bigInteger':'1001'}"; - String json = mapper.writeValueAsString(intialIntegralTypeWrapper); + String json = mapper.writeValueAsString(initialIntegralTypeWrapper); + + assertEquals(a2q(expectedJson), json); AllIntegralTypeWrapper readbackIntegralTypeWrapper = mapper.readValue(a2q(expectedJson), AllIntegralTypeWrapper.class); assertNotNull(readbackIntegralTypeWrapper); - assertEquals(intialIntegralTypeWrapper.byteValue, readbackIntegralTypeWrapper.byteValue); - assertEquals(intialIntegralTypeWrapper.ByteValue, readbackIntegralTypeWrapper.ByteValue); - assertEquals(intialIntegralTypeWrapper.shortValue, readbackIntegralTypeWrapper.shortValue); - assertEquals(intialIntegralTypeWrapper.ShortValue, readbackIntegralTypeWrapper.ShortValue); - assertEquals(intialIntegralTypeWrapper.intValue, readbackIntegralTypeWrapper.intValue); - assertEquals(intialIntegralTypeWrapper.IntegerValue, readbackIntegralTypeWrapper.IntegerValue); - assertEquals(intialIntegralTypeWrapper.longValue, readbackIntegralTypeWrapper.longValue); - assertEquals(intialIntegralTypeWrapper.LongValue, readbackIntegralTypeWrapper.LongValue); - assertEquals(intialIntegralTypeWrapper.bigInteger, readbackIntegralTypeWrapper.bigInteger); + assertEquals(initialIntegralTypeWrapper.byteValue, readbackIntegralTypeWrapper.byteValue); + assertEquals(initialIntegralTypeWrapper.ByteValue, readbackIntegralTypeWrapper.ByteValue); + assertEquals(initialIntegralTypeWrapper.shortValue, readbackIntegralTypeWrapper.shortValue); + assertEquals(initialIntegralTypeWrapper.ShortValue, readbackIntegralTypeWrapper.ShortValue); + assertEquals(initialIntegralTypeWrapper.intValue, readbackIntegralTypeWrapper.intValue); + assertEquals(initialIntegralTypeWrapper.IntegerValue, readbackIntegralTypeWrapper.IntegerValue); + assertEquals(initialIntegralTypeWrapper.longValue, readbackIntegralTypeWrapper.longValue); + assertEquals(initialIntegralTypeWrapper.LongValue, readbackIntegralTypeWrapper.LongValue); + assertEquals(initialIntegralTypeWrapper.bigInteger, readbackIntegralTypeWrapper.bigInteger); } } From 5db64fabb427ea814eebb2e7c59ce587bef865da Mon Sep 17 00:00:00 2001 From: Davyd Fridman Date: Tue, 16 Sep 2025 15:01:58 -0500 Subject: [PATCH 4/7] Fix #221 - address PR comments --- .../fasterxml/jackson/databind/cfg/BaseSettings.java | 12 ++++++------ .../fasterxml/jackson/databind/cfg/MapperConfig.java | 2 +- .../std/FromStringWithRadixToNumberDeserializer.java | 2 +- .../databind/deser/std/NumberDeserializers.java | 4 ++-- .../jackson/databind/ser/std/NumberSerializer.java | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java index 56c523d56c5..5587354e5ed 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java @@ -174,11 +174,11 @@ public final class BaseSettings * @since 2.21 */ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai, - PropertyNamingStrategy pns, EnumNamingStrategy ens, TypeFactory tf, - TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, - Locale locale, TimeZone tz, Base64Variant defaultBase64, - PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, - CacheProvider cacheProvider, int defaultRadix) + PropertyNamingStrategy pns, EnumNamingStrategy ens, TypeFactory tf, + TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi, + Locale locale, TimeZone tz, Base64Variant defaultBase64, + PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming, + CacheProvider cacheProvider, int defaultRadix) { _classIntrospector = ci; _annotationIntrospector = ai; @@ -546,7 +546,7 @@ public Base64Variant getBase64Variant() { } /** - * Method indicating number base to use for serializing/deserializing an integral number as a string. + * Method indicating base to use for serializing/deserializing an integral number as a string. * * @since 2.21 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java index 0bf2b5fb667..d11550f8cd2 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java @@ -672,7 +672,7 @@ public Base64Variant getBase64Variant() { } /** - * Method indicating which number base to use for serializing/deserializing an integral number as a string. + * Method indicating base to use for serializing/deserializing an integral number as a string. * * @since 2.21 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java index 7f50d225c49..bafcb69f127 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/FromStringWithRadixToNumberDeserializer.java @@ -8,7 +8,7 @@ import java.math.BigInteger; /** - * Deserializer used ii a string that represents a number in specific radix (base). + * Deserializer used for a string that represents a number in specific radix (base). * * @since 2.21 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java index 815282fb817..0d5e16db3e7 100644 --- a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java +++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java @@ -1125,7 +1125,7 @@ public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt) } /** - * Method used to create a string deserializer for a Number. + * Method used to create a string deserializer for a number. * If configuration is set properly, we create an alternative radix serializer {@link NumberToStringWithRadixSerializer}. * * @since 2.21 @@ -1151,7 +1151,7 @@ private static StdDeserializer _createRadixStringDeserializer( } /** - * Check if we have a proper {@link JsonFormat} annotation for serializing a Number + * Check if we have a proper {@link JsonFormat} annotation for serializing a number * using an alternative radix specified in the annotation. */ private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) { diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java index 5a3fbf08ed4..2dd1e0e7d08 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java @@ -118,7 +118,7 @@ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType t } /** - * Method used to create a string serializer for a Number. If the number is an integer, and configuration is set properly, + * Method used to create a string serializer for a number. If the number is integral, and configuration is set properly, * we create an alternative radix serializer {@link NumberToStringWithRadixSerializer}. * * @since 2.21 @@ -137,7 +137,7 @@ public static ToStringSerializerBase createStringSerializer(SerializerProvider p } /** - * Check if we have a proper {@link JsonFormat} annotation for serializing a Number + * Check if we have a proper {@link JsonFormat} annotation for serializing a number * using an alternative radix specified in the annotation. */ private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) { From 229d691e9a369d5675a7b25d1764e0e1bb17aa00 Mon Sep 17 00:00:00 2001 From: Davyd Fridman Date: Tue, 16 Sep 2025 15:22:37 -0500 Subject: [PATCH 5/7] Fix #221 - add test --- .../DifferentRadixNumberFormatTest.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java b/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java index 6a48f2b2e03..9ed5a1203da 100644 --- a/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java +++ b/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java @@ -42,6 +42,21 @@ public int getValue() { } } + private static class IncorrectlyAnnotatedMethodIntWrapper { + private int value; + + public IncorrectlyAnnotatedMethodIntWrapper() { + } + public IncorrectlyAnnotatedMethodIntWrapper(int v) { + value = v; + } + + @JsonFormat(shape = JsonFormat.Shape.STRING) + public int getValue() { + return value; + } + } + private static class AllIntegralTypeWrapper { @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2") public byte byteValue; @@ -138,6 +153,18 @@ void testAnnotatedAccessorSerializedAsHexString() assertEquals(initialIntWrapper.value, readBackIntWrapper.value); } + @Test + void testAnnotatedAccessorWithoutPatternDoesNotThrow() + throws JsonProcessingException { + ObjectMapper mapper = newJsonMapper(); + IncorrectlyAnnotatedMethodIntWrapper initialIntWrapper = new IncorrectlyAnnotatedMethodIntWrapper(10); + String expectedJson = "{'value':'10'}"; + + String json = mapper.writeValueAsString(initialIntWrapper); + + assertEquals(a2q(expectedJson), json); + } + @Test void testUsingBaseSettingRadixToSerializeAsHexString() throws JsonProcessingException { From 7dabb4b13f91959643f1a59b06a6d246de071b37 Mon Sep 17 00:00:00 2001 From: Davyd Fridman Date: Tue, 16 Sep 2025 17:49:58 -0500 Subject: [PATCH 6/7] Fix #221 - address PR comments --- src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java | 2 +- .../java/com/fasterxml/jackson/databind/cfg/BaseSettings.java | 2 +- .../java/com/fasterxml/jackson/databind/cfg/MapperConfig.java | 2 +- .../fasterxml/jackson/databind/ser/std/NumberSerializer.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java index acbfe9aaa93..1321de2dfdd 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java @@ -2549,7 +2549,7 @@ public ObjectMapper setDateFormat(DateFormat dateFormat) } /** - * Method for configuring the radix to use when serializing integral types + * Method for configuring the radix to use when serializing integer numbers * as strings. * * @since 2.21 diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java index 5587354e5ed..bec23c7e6b0 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java @@ -546,7 +546,7 @@ public Base64Variant getBase64Variant() { } /** - * Method indicating base to use for serializing/deserializing an integral number as a string. + * Method indicating base to use for serializing/deserializing integer numbers as a string. * * @since 2.21 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java index d11550f8cd2..42a5594dddf 100644 --- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java @@ -672,7 +672,7 @@ public Base64Variant getBase64Variant() { } /** - * Method indicating base to use for serializing/deserializing an integral number as a string. + * Method indicating base to use for serializing/deserializing integer numbers as a string. * * @since 2.21 */ diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java index 2dd1e0e7d08..b4047bdfa22 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java +++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java @@ -118,7 +118,7 @@ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType t } /** - * Method used to create a string serializer for a number. If the number is integral, and configuration is set properly, + * Method used to create a string serializer for a number. If the number is integer, and configuration is set properly, * we create an alternative radix serializer {@link NumberToStringWithRadixSerializer}. * * @since 2.21 From 5599e797dbb97eade55949a3a47afbdb854320db Mon Sep 17 00:00:00 2001 From: Davyd Fridman Date: Fri, 19 Sep 2025 14:27:54 -0500 Subject: [PATCH 7/7] Fix #221 - wire default radix via ConfigOverrides --- .../databind/DeserializationConfig.java | 5 - .../jackson/databind/ObjectMapper.java | 36 ++-- .../jackson/databind/SerializationConfig.java | 5 - .../jackson/databind/cfg/BaseSettings.java | 86 ++------- .../jackson/databind/cfg/ConfigOverrides.java | 44 ++++- .../jackson/databind/cfg/MapperBuilder.java | 15 ++ .../jackson/databind/cfg/MapperConfig.java | 17 +- .../databind/cfg/MapperConfigBase.java | 13 +- .../deser/std/NumberDeserializers.java | 14 -- .../introspect/ConcreteBeanPropertyBase.java | 15 +- .../databind/ser/std/NumberSerializer.java | 22 +-- .../NumberToStringWithRadixSerializer.java | 5 +- .../databind/SerializeUsingJDKTest.java | 2 + .../DifferentRadixNumberFormatTest.java | 31 +-- .../ConcreteBeanPropertyBaseTest.java | 181 ++++++++++++++++++ 15 files changed, 326 insertions(+), 165 deletions(-) create mode 100644 src/test/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBaseTest.java diff --git a/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java b/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java index 56da80064b4..f32e2028efe 100644 --- a/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java +++ b/src/main/java/com/fasterxml/jackson/databind/DeserializationConfig.java @@ -373,11 +373,6 @@ public DeserializationConfig withRootName(PropertyName rootName) { return new DeserializationConfig(this, rootName); } - @Override - public DeserializationConfig withRadix(int radix) { - return _withBase(_base.withRadix(radix)); - } - @Override public DeserializationConfig withView(Class view) { return (_view == view) ? this : new DeserializationConfig(this, view); diff --git a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java index 1321de2dfdd..10cda9efc6a 100644 --- a/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java +++ b/src/main/java/com/fasterxml/jackson/databind/ObjectMapper.java @@ -402,7 +402,6 @@ public boolean useForType(JavaType t) // 16-May-2009, tatu: Ditto ^^^ protected final static AnnotationIntrospector DEFAULT_ANNOTATION_INTROSPECTOR = new JacksonAnnotationIntrospector(); - protected static final int DEFAULT_RADIX = 10; /** * Base settings contain defaults used for all {@link ObjectMapper} * instances. @@ -420,9 +419,7 @@ public boolean useForType(JavaType t) // Since 2.12: new DefaultAccessorNamingStrategy.Provider(), // Since 2.16: [databind#2502] Add a way to configure Caches Jackson uses - DefaultCacheProvider.defaultInstance(), - //since 2.21: [databind#221] - support alternate radixes for numerical values serialized as strings - DEFAULT_RADIX + DefaultCacheProvider.defaultInstance() ); /* @@ -1899,6 +1896,24 @@ public ObjectMapper setDefaultPropertyInclusion(JsonInclude.Value incl) { return this; } + /** + * Method for setting default alternative radix that applies to integral types for serialization + * and deserialization of such types as strings. + * This configuration override is applied for all integral properties for which there are no per-type + * or per-property overrides (via annotations or config overrides). + *

+ * NOTE: in Jackson 3.x all configuration goes through {@code ObjectMapper} builders, + * see {@link com.fasterxml.jackson.databind.cfg.MapperBuilder}, + * and this method will be removed from 3.0. + * + * @since 2.21 + */ + public ObjectMapper setDefaultFormat(String radix) { + _configOverrides.setDefaultRadix(radix); + return this; + } + + /** * Short-cut for: *

@@ -2548,19 +2563,6 @@ public ObjectMapper setDateFormat(DateFormat dateFormat)
         return this;
     }
 
-    /**
-     * Method for configuring the radix to use when serializing integer numbers
-     * as strings.
-     *
-     * @since 2.21
-     */
-    public ObjectMapper setRadix(int radix)
-    {
-        _deserializationConfig = _deserializationConfig.withRadix(radix);
-        _serializationConfig = _serializationConfig.withRadix(radix);
-        return this;
-    }
-
     /**
      * @since 2.5
      */
diff --git a/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java b/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java
index ec3737f79b1..d12733629a5 100644
--- a/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java
+++ b/src/main/java/com/fasterxml/jackson/databind/SerializationConfig.java
@@ -371,11 +371,6 @@ public SerializationConfig with(SubtypeResolver str) {
         return (str == _subtypeResolver)? this : new SerializationConfig(this, str);
     }
 
-    @Override
-    public SerializationConfig withRadix(int radix) {
-        return _withBase(_base.withRadix(radix));
-    }
-
     @Override
     public SerializationConfig withView(Class view) {
         return (_view == view) ? this : new SerializationConfig(this, view);
diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java
index bec23c7e6b0..e2e66cb5241 100644
--- a/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java
+++ b/src/main/java/com/fasterxml/jackson/databind/cfg/BaseSettings.java
@@ -35,8 +35,6 @@ public final class BaseSettings
              */
             TimeZone.getTimeZone("UTC");
 
-    private static final int DEFAULT_RADIX = 10;
-
     /*
     /**********************************************************
     /* Configuration settings; introspection, related
@@ -157,13 +155,6 @@ public final class BaseSettings
      */
     protected final CacheProvider _cacheProvider;
 
-    /**
-     * Default radix to use when serializing/deserializing integers to string.
-     *
-     * @since 2.21
-     */
-    protected final int _defaultRadix;
-
     /*
     /**********************************************************
     /* Construction
@@ -171,14 +162,14 @@ public final class BaseSettings
      */
 
     /**
-     * @since 2.21
+     * @since 2.19
      */
     public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai,
             PropertyNamingStrategy pns, EnumNamingStrategy ens, TypeFactory tf,
             TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi,
             Locale locale, TimeZone tz, Base64Variant defaultBase64,
             PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming,
-            CacheProvider cacheProvider, int defaultRadix)
+            CacheProvider cacheProvider)
     {
         _classIntrospector = ci;
         _annotationIntrospector = ai;
@@ -193,22 +184,7 @@ public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai,
         _defaultBase64 = defaultBase64;
         _typeValidator = ptv;
         _accessorNaming = accNaming;
-        _cacheProvider = cacheProvider;
-        _defaultRadix = defaultRadix;
-    }
-
-    /**
-     * @since 2.19
-     * @deprecated since 2.21, use variant that takes defaultRadix
-     */
-    public BaseSettings(ClassIntrospector ci, AnnotationIntrospector ai,
-            PropertyNamingStrategy pns, EnumNamingStrategy ens, TypeFactory tf,
-            TypeResolverBuilder typer, DateFormat dateFormat, HandlerInstantiator hi,
-            Locale locale, TimeZone tz, Base64Variant defaultBase64,
-            PolymorphicTypeValidator ptv, AccessorNamingStrategy.Provider accNaming,
-            CacheProvider cacheProvider)
-    {
-        this(ci, ai, pns, ens, tf, typer, dateFormat, hi, locale, tz, defaultBase64, ptv, accNaming, cacheProvider, DEFAULT_RADIX);
+        _cacheProvider = cacheProvider;;
     }
 
     /**
@@ -261,8 +237,7 @@ public BaseSettings copy() {
             _defaultBase64,
             _typeValidator,
             _accessorNaming,
-            _cacheProvider,
-            _defaultRadix);
+            _cacheProvider);
     }
 
     /*
@@ -277,7 +252,7 @@ public BaseSettings withClassIntrospector(ClassIntrospector ci) {
         }
         return new BaseSettings(ci, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy,
                 _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     public BaseSettings withAnnotationIntrospector(AnnotationIntrospector ai) {
@@ -286,7 +261,7 @@ public BaseSettings withAnnotationIntrospector(AnnotationIntrospector ai) {
         }
         return new BaseSettings(_classIntrospector, ai, _propertyNamingStrategy, _enumNamingStrategy,
                 _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     public BaseSettings withInsertedAnnotationIntrospector(AnnotationIntrospector ai) {
@@ -313,7 +288,7 @@ public BaseSettings withPropertyNamingStrategy(PropertyNamingStrategy pns) {
         }
         return new BaseSettings(_classIntrospector, _annotationIntrospector, pns, _enumNamingStrategy,
                 _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     /**
@@ -325,7 +300,7 @@ public BaseSettings withEnumNamingStrategy(EnumNamingStrategy ens) {
         }
         return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, ens,
                 _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     // @since 2.12
@@ -335,7 +310,7 @@ public BaseSettings withAccessorNaming(AccessorNamingStrategy.Provider p) {
         }
         return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy,
                 _typeFactory, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, _typeValidator, p, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, p, _cacheProvider);
     }
 
     public BaseSettings withTypeFactory(TypeFactory tf) {
@@ -344,7 +319,7 @@ public BaseSettings withTypeFactory(TypeFactory tf) {
         }
         return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy,
                 tf, _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     public BaseSettings withTypeResolverBuilder(TypeResolverBuilder typer) {
@@ -353,7 +328,7 @@ public BaseSettings withTypeResolverBuilder(TypeResolverBuilder typer) {
         }
         return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy,
                 _typeFactory, typer, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     public BaseSettings withDateFormat(DateFormat df) {
@@ -367,7 +342,7 @@ public BaseSettings withDateFormat(DateFormat df) {
         }
         return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy,
                 _typeFactory, _typeResolverBuilder, df, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     public BaseSettings withHandlerInstantiator(HandlerInstantiator hi) {
@@ -376,7 +351,7 @@ public BaseSettings withHandlerInstantiator(HandlerInstantiator hi) {
         }
         return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy,
                 _typeFactory, _typeResolverBuilder, _dateFormat, hi, _locale,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     public BaseSettings with(Locale l) {
@@ -385,7 +360,7 @@ public BaseSettings with(Locale l) {
         }
         return new BaseSettings(_classIntrospector, _annotationIntrospector, _propertyNamingStrategy, _enumNamingStrategy,
                 _typeFactory,_typeResolverBuilder, _dateFormat, _handlerInstantiator, l,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     /**
@@ -408,7 +383,7 @@ public BaseSettings with(TimeZone tz)
         return new BaseSettings(_classIntrospector, _annotationIntrospector,
                 _propertyNamingStrategy, _enumNamingStrategy, _typeFactory,
                 _typeResolverBuilder, df, _handlerInstantiator, _locale,
-                tz, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                tz, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     /**
@@ -421,7 +396,7 @@ public BaseSettings with(Base64Variant base64) {
         return new BaseSettings(_classIntrospector, _annotationIntrospector,
                 _propertyNamingStrategy, _enumNamingStrategy, _typeFactory,
                 _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, base64, _typeValidator, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, base64, _typeValidator, _accessorNaming, _cacheProvider);
     }
 
     /**
@@ -434,7 +409,7 @@ public BaseSettings with(PolymorphicTypeValidator v) {
         return new BaseSettings(_classIntrospector, _annotationIntrospector,
                 _propertyNamingStrategy, _enumNamingStrategy, _typeFactory,
                 _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, v, _accessorNaming, _cacheProvider, _defaultRadix);
+                _timeZone, _defaultBase64, v, _accessorNaming, _cacheProvider);
     }
 
     /**
@@ -450,23 +425,7 @@ public BaseSettings with(CacheProvider cacheProvider) {
         return new BaseSettings(_classIntrospector, _annotationIntrospector,
                 _propertyNamingStrategy, _enumNamingStrategy, _typeFactory,
                 _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, cacheProvider, _defaultRadix);
-    }
-
-    /**
-     * Fluent factory for constructing a new instance with provided default radix.
-     *
-     * @return a new instance with provided defaultRadix.
-     * @since 2.21
-     */
-    public BaseSettings withRadix(int defaultRadix) {
-        if (defaultRadix == _defaultRadix) {
-            return this;
-        }
-        return new BaseSettings(_classIntrospector, _annotationIntrospector,
-                _propertyNamingStrategy, _enumNamingStrategy, _typeFactory,
-                _typeResolverBuilder, _dateFormat, _handlerInstantiator, _locale,
-                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, _cacheProvider, defaultRadix);
+                _timeZone, _defaultBase64, _typeValidator, _accessorNaming, cacheProvider);
     }
 
     /*
@@ -544,15 +503,6 @@ public boolean hasExplicitTimeZone() {
     public Base64Variant getBase64Variant() {
         return _defaultBase64;
     }
-
-    /**
-     * Method indicating base to use for serializing/deserializing integer numbers as a string.
-     *
-     * @since 2.21
-     */
-    public int getRadix() {
-        return _defaultRadix;
-    }
     
     /**
      * @since 2.16
diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java b/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java
index 4961de2386f..4fed2bc0446 100644
--- a/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java
+++ b/src/main/java/com/fasterxml/jackson/databind/cfg/ConfigOverrides.java
@@ -17,6 +17,7 @@ public class ConfigOverrides
     implements java.io.Serializable
 {
     private static final long serialVersionUID = 1L;
+    private static final String DEFAULT_RADIX = "10";
 
     /**
      * Per-type override definitions
@@ -55,6 +56,14 @@ public class ConfigOverrides
      */
     protected Boolean _defaultLeniency;
 
+    /**
+     * Global default radix to apply to an integral type outputted as string. This has the lowest precedence out of all
+     * other methods of enforcing an alternative radix.
+     *
+     * @since 2.21
+     */
+    protected String _defaultRadix;//TODO(Davyd Fridman): Change from string to int once JsonFormat has an actual radix field
+
     /*
     /**********************************************************************
     /* Life cycle
@@ -67,13 +76,32 @@ public ConfigOverrides() {
                 JsonInclude.Value.empty(),
                 JsonSetter.Value.empty(),
                 VisibilityChecker.Std.defaultInstance(),
-                null, null
+                null, null, DEFAULT_RADIX
         );
     }
 
+    /**
+     * @since 2.21
+     */
+    protected ConfigOverrides(Map, MutableConfigOverride> overrides,
+                              JsonInclude.Value defIncl, JsonSetter.Value defSetter,
+                              VisibilityChecker defVisibility, Boolean defMergeable, Boolean defLeniency,
+                              String defRadix)
+    {
+        _overrides = overrides;
+        _defaultInclusion = defIncl;
+        _defaultSetterInfo = defSetter;
+        _visibilityChecker = defVisibility;
+        _defaultMergeable = defMergeable;
+        _defaultLeniency = defLeniency;
+        _defaultRadix = defRadix;
+    }
+
     /**
      * @since 2.10
+     * @deprecated since 2.21
      */
+    @Deprecated
     protected ConfigOverrides(Map, MutableConfigOverride> overrides,
             JsonInclude.Value defIncl, JsonSetter.Value defSetter,
             VisibilityChecker defVisibility, Boolean defMergeable, Boolean defLeniency)
@@ -197,6 +225,13 @@ public VisibilityChecker getDefaultVisibility() {
         return _visibilityChecker;
     }
 
+    /**
+     * @since 2.21
+     */
+    public String getDefaultRadix() {
+        return _defaultRadix;
+    }
+
     /**
      * @since 2.9
      */
@@ -232,6 +267,13 @@ public void setDefaultVisibility(VisibilityChecker v) {
         _visibilityChecker = v;
     }
 
+    /**
+     * @since 2.21
+     */
+    public void setDefaultRadix(String v) {
+        this._defaultRadix = v;
+    }
+
     /*
     /**********************************************************************
     /* Helper methods
diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperBuilder.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperBuilder.java
index cbfe4277d2d..cc9f7fa6135 100644
--- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperBuilder.java
+++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperBuilder.java
@@ -7,6 +7,7 @@
 import java.util.function.Consumer;
 
 import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonSetter;
 import com.fasterxml.jackson.annotation.JsonTypeInfo;
@@ -747,6 +748,20 @@ public B defaultPropertyInclusion(JsonInclude.Value incl) {
         return _this();
     }
 
+    /**
+     * Method for configured default radix to use for serialization/deserialization of integral types as strings.
+     *
+     * @param radix Default radix to use on integral properties
+     *
+     * @return This builder instance to allow call chaining
+     *
+     * @since 2.11
+     */
+    public B defaultFormat(String radix) {
+        _mapper.setDefaultFormat(radix);
+        return _this();
+    }
+
     /*
     /**********************************************************************
     /* Configuring Mix-ins
diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java
index 42a5594dddf..97a861601c4 100644
--- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java
+++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfig.java
@@ -507,6 +507,14 @@ public JsonInclude.Value getDefaultInclusion(Class baseType,
         return result;
     }
 
+    /**
+     * Accessor for default radix to apply to integral types when serializing them as string.
+     * The radix obtained from this accessor should have the lowest precedence.
+     *
+     * @since 2.21
+     */
+    public abstract String getDefaultRadix();
+
     /**
      * Accessor for default format settings to use for serialization (and, to a degree
      * deserialization), considering baseline settings and per-type defaults
@@ -671,15 +679,6 @@ public Base64Variant getBase64Variant() {
         return _base.getBase64Variant();
     }
 
-    /**
-     * Method indicating base to use for serializing/deserializing integer numbers as a string.
-     *
-     * @since 2.21
-     */
-    public int getRadix() {
-        return _base.getRadix();
-    }
-
     /**
      * Method for accessing per-instance shared (baseline/default)
      * attribute values; these are used as the basis for per-call
diff --git a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java
index 4a13626ca95..b0fcdb34565 100644
--- a/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java
+++ b/src/main/java/com/fasterxml/jackson/databind/cfg/MapperConfigBase.java
@@ -682,14 +682,6 @@ public T withRootName(String rootName) {
      */
     public abstract T with(SubtypeResolver str);
 
-    /**
-     * Method for constructing and returning a new instance with different
-     * radix to use.
-     *
-     * @since 2.21
-     */
-    public abstract T withRadix(int radix);
-
     /**
      * Method for constructing and returning a new instance with different
      * view to use.
@@ -785,6 +777,11 @@ public final JsonInclude.Value getDefaultInclusion(Class baseType,
         return def.withOverrides(v);
     }
 
+    @Override
+    public String getDefaultRadix() {
+        return _configOverrides.getDefaultRadix();
+    }
+
     @Override
     public final JsonFormat.Value getDefaultPropertyFormat(Class type) {
         return _configOverrides.findFormatDefaults(type);
diff --git a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java
index 0d5e16db3e7..4ba35ea8683 100644
--- a/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java
+++ b/src/main/java/com/fasterxml/jackson/databind/deser/std/NumberDeserializers.java
@@ -1142,9 +1142,6 @@ private static StdDeserializer _createRadixStringDeserializer(
         if (isSerializeWithRadixOverride(format)) {
             int radix = Integer.parseInt(format.getPattern());
             return new FromStringWithRadixToNumberDeserializer(initialDeser, radix);
-        } else if (isSerializeWithDefaultConfigOverride(ctxt)) {
-            int radix = ctxt.getConfig().getRadix();
-            return new FromStringWithRadixToNumberDeserializer(initialDeser, radix);
         }
 
         return initialDeser;
@@ -1164,15 +1161,4 @@ private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) {
         int radix = Integer.parseInt(pattern);
         return radix != DEFAULT_RADIX;
     }
-
-    /**
-     * Check if we have a non-default radix specified as part of {@link com.fasterxml.jackson.databind.cfg.BaseSettings} contained
-     * in {@link DeserializationConfig}.
-     */
-    private static boolean isSerializeWithDefaultConfigOverride(DeserializationContext ctxt) {
-        if (ctxt.getConfig() == null) {
-            return false;
-        }
-        return ctxt.getConfig().getRadix() != DEFAULT_RADIX;
-    }
 }
diff --git a/src/main/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBase.java b/src/main/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBase.java
index 315bb76906c..fbe786be567 100644
--- a/src/main/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBase.java
+++ b/src/main/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBase.java
@@ -71,6 +71,7 @@ public final JsonFormat.Value findFormatOverrides(AnnotationIntrospector intr) {
     @Override
     public JsonFormat.Value findPropertyFormat(MapperConfig config, Class baseType)
     {
+        JsonFormat.Value v0 = EMPTY_FORMAT.withPattern(config.getDefaultRadix());//TODO(Davyd Fridman): change to withRadix
         JsonFormat.Value v1 = config.getDefaultPropertyFormat(baseType);
         JsonFormat.Value v2 = null;
         AnnotationIntrospector intr = config.getAnnotationIntrospector();
@@ -80,10 +81,18 @@ public JsonFormat.Value findPropertyFormat(MapperConfig config, Class base
                 v2 = intr.findFormat(member);
             }
         }
-        if (v1 == null) {
-            return (v2 == null) ? EMPTY_FORMAT : v2;
+
+        JsonFormat.Value formatValue = EMPTY_FORMAT;
+        if (v0 != null) {
+            formatValue = formatValue.withOverrides(v0);
+        }
+        if (v1 != null) {
+            formatValue = formatValue.withOverrides(v1);
+        }
+        if (v2 != null) {
+            formatValue = formatValue.withOverrides(v2);
         }
-        return (v2 == null) ? v1 : v1.withOverrides(v2);
+        return formatValue;
     }
 
     @Override
diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java
index b4047bdfa22..6e970f88e56 100644
--- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java
+++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberSerializer.java
@@ -124,14 +124,9 @@ public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType t
      * @since 2.21
      */
     public static ToStringSerializerBase createStringSerializer(SerializerProvider prov, JsonFormat.Value format, boolean isInt) {
-        if (isInt) {
-            if (isSerializeWithRadixOverride(format)) {
-                int radix = Integer.parseInt(format.getPattern());
-                return new NumberToStringWithRadixSerializer(radix);
-            } else if (isSerializeWithDefaultConfigOverride(prov)) {
-                int radix = prov.getConfig().getRadix();
-                return new NumberToStringWithRadixSerializer(radix);
-            }
+        if (isInt && isSerializeWithRadixOverride(format)) {
+            int radix = Integer.parseInt(format.getPattern());
+            return new NumberToStringWithRadixSerializer(radix);
         }
         return ToStringSerializer.instance;
     }
@@ -151,17 +146,6 @@ private static boolean isSerializeWithRadixOverride(JsonFormat.Value format) {
         return radix != DEFAULT_RADIX;
     }
 
-    /**
-     * Check if we have a non-default radix specified as part of {@link com.fasterxml.jackson.databind.cfg.BaseSettings} contained
-     * in {@link SerializationConfig}.
-     */
-    private static boolean isSerializeWithDefaultConfigOverride(SerializerProvider prov) {
-        if (prov.getConfig() == null) {
-            return false;
-        }
-        return prov.getConfig().getRadix() != DEFAULT_RADIX;
-    }
-
     /**
      * @since 2.10
      */
diff --git a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java
index 29f79a71c3f..edef76bc23d 100644
--- a/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java
+++ b/src/main/java/com/fasterxml/jackson/databind/ser/std/NumberToStringWithRadixSerializer.java
@@ -41,8 +41,9 @@ public void serialize(Object value, JsonGenerator gen, SerializerProvider provid
         }
 
         String text = "";
-        if (value instanceof BigInteger bigInt) {
-            text = bigInt.toString(radix);
+        if (value instanceof BigInteger) {
+            BigInteger bigIntegerValue = (BigInteger) value;
+            text = bigIntegerValue.toString(radix);
         } else if (value instanceof Byte
                 || value instanceof Short
                 || value instanceof Integer
diff --git a/src/test/java/com/fasterxml/jackson/databind/SerializeUsingJDKTest.java b/src/test/java/com/fasterxml/jackson/databind/SerializeUsingJDKTest.java
index d607fc4f768..f8943f39c71 100644
--- a/src/test/java/com/fasterxml/jackson/databind/SerializeUsingJDKTest.java
+++ b/src/test/java/com/fasterxml/jackson/databind/SerializeUsingJDKTest.java
@@ -238,6 +238,8 @@ public void testTypeFactory() throws Exception
     public void testObjectReaderSerializationWithPolymorphism()
         throws Exception
     {
+        Properties props = System.getProperties();
+        props.setProperty("sun.io.serialization.extendedDebugInfo", "true");
         Class[] classes = new Class[] {
             FooClass.class,
             FooDeduction.class,
diff --git a/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java b/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java
index 9ed5a1203da..4ab8783d05b 100644
--- a/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java
+++ b/src/test/java/com/fasterxml/jackson/databind/format/DifferentRadixNumberFormatTest.java
@@ -13,6 +13,9 @@
 
 public class DifferentRadixNumberFormatTest extends DatabindTestUtil {
 
+    private static final String HEX_RADIX = "16";
+    public static final String BINARY_RADIX = "2";
+
     private static class IntegerWrapper {
         public Integer value;
 
@@ -36,7 +39,7 @@ public AnnotatedMethodIntWrapper(int v) {
             value = v;
         }
 
-        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "16")
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = HEX_RADIX)
         public int getValue() {
             return value;
         }
@@ -58,27 +61,27 @@ public int getValue() {
     }
 
     private static class AllIntegralTypeWrapper {
-        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2")
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX)
         public byte byteValue;
-        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2")
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX)
         public Byte ByteValue;
 
-        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2")
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX)
         public short shortValue;
-        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2")
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX)
         public Short ShortValue;
 
-        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2")
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX)
         public int intValue;
-        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2")
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX)
         public Integer IntegerValue;
 
-        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2")
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX)
         public long longValue;
-        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2")
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX)
         public Long LongValue;
 
-        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "2")
+        @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = BINARY_RADIX)
         public BigInteger bigInteger;
 
         public AllIntegralTypeWrapper() {
@@ -102,7 +105,7 @@ public AllIntegralTypeWrapper(byte byteValue, Byte ByteValue, short shortValue,
     void testIntegerSerializedAsHexString()
             throws JsonProcessingException {
         ObjectMapper mapper = newJsonMapper();
-        mapper.configOverride(Integer.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING).withPattern("16"));
+        mapper.configOverride(Integer.class).setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING).withPattern(HEX_RADIX));
         IntegerWrapper initialIntegerWrapper = new IntegerWrapper(10);
         String json = mapper.writeValueAsString(initialIntegerWrapper);
         String expectedJson = "{'value':'a'}";
@@ -121,7 +124,7 @@ void testIntSerializedAsHexString()
             throws JsonProcessingException {
         ObjectMapper mapper = newJsonMapper();
         mapper.configOverride(int.class)
-              .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING).withPattern("16"));
+              .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING).withPattern(HEX_RADIX));
         IntWrapper intialIntWrapper = new IntWrapper(10);
         String expectedJson = "{'value':'a'}";
 
@@ -166,12 +169,12 @@ void testAnnotatedAccessorWithoutPatternDoesNotThrow()
     }
 
     @Test
-    void testUsingBaseSettingRadixToSerializeAsHexString()
+    void testUsingDefaultConfigOverrideRadixToSerializeAsHexString()
             throws JsonProcessingException {
         ObjectMapper mapper = newJsonMapper();
         mapper.configOverride(Integer.class)
               .setFormat(JsonFormat.Value.forShape(JsonFormat.Shape.STRING));
-        mapper.setRadix(16);
+        mapper.setDefaultFormat(HEX_RADIX);
         IntegerWrapper intialIntegerWrapper = new IntegerWrapper(10);
         String expectedJson = "{'value':'a'}";
 
diff --git a/src/test/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBaseTest.java b/src/test/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBaseTest.java
new file mode 100644
index 00000000000..f7fe25d607b
--- /dev/null
+++ b/src/test/java/com/fasterxml/jackson/databind/introspect/ConcreteBeanPropertyBaseTest.java
@@ -0,0 +1,181 @@
+package com.fasterxml.jackson.databind.introspect;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.AnnotationIntrospector;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.PropertyMetadata;
+import com.fasterxml.jackson.databind.PropertyName;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.cfg.MapperConfig;
+import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Member;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+//TODO: Once mockito is updated to include Premain-Class in its MANIFEST.MF, we need to add -javaagent:/${m2directory}/.m2/repository/org/mockito/mockito-core/${mockit-version}/$33{mockit-version}.jar
+class ConcreteBeanPropertyBaseTest {
+
+    private static final class TestConcreteBeanPropertyBase extends ConcreteBeanPropertyBase {
+
+        TestConcreteBeanPropertyBase(PropertyMetadata md) {
+            super(md);
+        }
+
+        @Override
+        public String getName() {
+            return "";
+        }
+
+        @Override
+        public PropertyName getFullName() {
+            return null;
+        }
+
+        @Override
+        public JavaType getType() {
+            return null;
+        }
+
+        @Override
+        public PropertyName getWrapperName() {
+            return null;
+        }
+
+        @Override
+        public  A getAnnotation(Class acls) {
+            return null;
+        }
+
+        @Override
+        public  A getContextAnnotation(Class acls) {
+            return null;
+        }
+
+        @Override
+        public AnnotatedMember getMember() {
+            return new TestAnnotatedMember(null, null);
+        }
+
+        @Override
+        public void depositSchemaProperty(JsonObjectFormatVisitor objectVisitor, SerializerProvider provider)
+                throws JsonMappingException {
+
+        }
+    }
+
+    private static final class TestAnnotatedMember extends AnnotatedMember {
+
+        TestAnnotatedMember(TypeResolutionContext ctxt, AnnotationMap annotations) {
+            super(ctxt, annotations);
+        }
+
+        @Override
+        public Annotated withAnnotations(AnnotationMap fallback) {
+            return null;
+        }
+
+        @Override
+        public Class getDeclaringClass() {
+            return null;
+        }
+
+        @Override
+        public Member getMember() {
+            return null;
+        }
+
+        @Override
+        public void setValue(Object pojo, Object value)
+                throws UnsupportedOperationException, IllegalArgumentException {
+
+        }
+
+        @Override
+        public Object getValue(Object pojo)
+                throws UnsupportedOperationException, IllegalArgumentException {
+            return null;
+        }
+
+        @Override
+        public AnnotatedElement getAnnotated() {
+            return null;
+        }
+
+        @Override
+        protected int getModifiers() {
+            return 0;
+        }
+
+        @Override
+        public String getName() {
+            return "";
+        }
+
+        @Override
+        public JavaType getType() {
+            return null;
+        }
+
+        @Override
+        public Class getRawType() {
+            return null;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return 0;
+        }
+
+        @Override
+        public String toString() {
+            return "";
+        }
+    }
+
+    private TestConcreteBeanPropertyBase testConcreteBeanProperty;
+    private Class someType;
+    private MapperConfig mapperConfig;
+    private AnnotationIntrospector annotationIntrospector;
+
+    @BeforeEach
+    void setUp() {
+        mapperConfig = mock(MapperConfig.class);
+        testConcreteBeanProperty =  new TestConcreteBeanPropertyBase(
+                PropertyMetadata.STD_REQUIRED);
+        annotationIntrospector = mock(AnnotationIntrospector.class);
+        when(mapperConfig.getAnnotationIntrospector()).thenReturn(annotationIntrospector);
+        someType = Class.class;
+    }
+
+    @Test
+    void testFormatPrecedenceIsFollowed() {
+        String lowestPrecedenceFormat = "Low Precedence";
+        JsonFormat.Value midPrecedenceFormat = new JsonFormat.Value("Mid Precedence", null,
+                (String) null, null, null, null);
+        JsonFormat.Value highestPrecedence = new JsonFormat.Value("High Precedence", null,
+                (String) null, null, null, null);
+        when(mapperConfig.getDefaultRadix()).thenReturn(lowestPrecedenceFormat);
+        when(mapperConfig.getDefaultPropertyFormat(any())).thenReturn(midPrecedenceFormat);
+        when(annotationIntrospector.findFormat(any())).thenReturn(highestPrecedence);
+
+        JsonFormat.Value resultFormat = testConcreteBeanProperty.findPropertyFormat(mapperConfig, someType);
+
+
+        assertEquals(highestPrecedence.getPattern(), resultFormat.getPattern());
+    }
+}
\ No newline at end of file