Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 40 additions & 4 deletions src/main/java/tools/jackson/databind/cfg/ConfigOverrides.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,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 3.1
*/
protected int _defaultRadix;

/*
/**********************************************************************
/* Life cycle
Expand All @@ -64,20 +72,32 @@ public ConfigOverrides() {
INCLUDE_DEFAULT,
JsonSetter.Value.empty(),
DEFAULT_VISIBILITY_CHECKER,
null, null
null, null, JsonFormat.DEFAULT_RADIX
);
}

protected ConfigOverrides(Map<Class<?>, MutableConfigOverride> overrides,
JsonInclude.Value defIncl, JsonSetter.Value defSetter,
VisibilityChecker defVisibility,
Boolean defMergeable, Boolean defLeniency) {
JsonInclude.Value defIncl, JsonSetter.Value defSetter,
VisibilityChecker defVisibility,
Boolean defMergeable, Boolean defLeniency, int defRadix) {
_overrides = overrides;
_defaultInclusion = defIncl;
_defaultNullHandling = defSetter;
_visibilityChecker = defVisibility;
_defaultMergeable = defMergeable;
_defaultLeniency = defLeniency;
_defaultRadix = defRadix;
}

@Deprecated
/*
* @deprecated since 3.1
*/
protected ConfigOverrides(Map<Class<?>, MutableConfigOverride> overrides,
JsonInclude.Value defIncl, JsonSetter.Value defSetter,
VisibilityChecker defVisibility,
Boolean defMergeable, Boolean defLeniency) {
this(overrides, defIncl, defSetter, defVisibility, defMergeable, defLeniency, JsonFormat.DEFAULT_RADIX);
}

@Override
Expand Down Expand Up @@ -176,6 +196,14 @@ public VisibilityChecker getDefaultVisibility() {
return _visibilityChecker;
}


/**
* @since 3.1
*/
public int getDefaultRadix() {
return _defaultRadix;
}

/**
* Alternate accessor needed due to complexities of Record
* auto-discovery: needs to obey custom overrides but also
Expand Down Expand Up @@ -222,6 +250,14 @@ public ConfigOverrides setDefaultVisibility(VisibilityChecker v) {
return this;
}

/**
* @since 3.1
*/
public ConfigOverrides setDefaultRadix(int v) {
this._defaultRadix = v;
return this;
}

public ConfigOverrides setDefaultVisibility(JsonAutoDetect.Value vis) {
_visibilityChecker = VisibilityChecker.construct(vis);
return this;
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/tools/jackson/databind/cfg/MapperBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,20 @@ public B changeDefaultPropertyInclusion(UnaryOperator<JsonInclude.Value> handler
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 3.1
*/
public B defaultRadix(int radix) {
_configOverrides.setDefaultRadix(radix);
return _this();
}

/**
* Method for changing currently default settings for handling of `null` values during
* deserialization, regarding whether they are set as-is, ignored completely, or possible
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/tools/jackson/databind/cfg/MapperConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,15 @@ 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 3.1
*/
public abstract int getDefaultRadix();

/**
* Accessor for default format settings to use for serialization (and, to a degree
* deserialization), considering baseline settings and per-type defaults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,11 @@ public final JsonInclude.Value getDefaultInclusion(Class<?> baseType,
return def.withOverrides(v);
}

@Override
public int getDefaultRadix() {
return _configOverrides.getDefaultRadix();
}

@Override
public final JsonFormat.Value getDefaultPropertyFormat(Class<?> type) {
return _configOverrides.findFormatDefaults(type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import tools.jackson.databind.util.AccessPattern;
import tools.jackson.databind.util.ClassUtil;

import static tools.jackson.databind.deser.std.RadixSerializerCreator.createRadixStringDeserializer;

/**
* Container class for deserializers that handle core JDK primitive
* (and matching wrapper) types, as well as standard "big" numeric types.
Expand Down Expand Up @@ -261,6 +263,16 @@ public Byte deserialize(JsonParser p, DeserializationContext ctxt) throws Jackso
return _parseByte(p, ctxt);
}


/**
* @since 3.1
*/
@Override
public ValueDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
{
return createRadixStringDeserializer(this, ctxt, property);
}

protected Byte _parseByte(JsonParser p, DeserializationContext ctxt)
throws JacksonException
{
Expand Down Expand Up @@ -346,6 +358,15 @@ public ShortDeserializer(Class<Short> cls, Short nvl)
super(cls, LogicalType.Integer, nvl, (short)0);
}

/**
* @since 3.1
*/
@Override
public ValueDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
{
return createRadixStringDeserializer(this, ctxt, property);
}

@Override
public Short deserialize(JsonParser p, DeserializationContext ctxt)
throws JacksonException
Expand Down Expand Up @@ -526,6 +547,15 @@ public IntegerDeserializer(Class<Integer> cls, Integer nvl) {
@Override
public boolean isCachable() { return true; }

/**
* @since 3.1
*/
@Override
public ValueDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
{
return createRadixStringDeserializer(this, ctxt, property);
}

@Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
if (p.isExpectedNumberIntToken()) {
Expand Down Expand Up @@ -567,6 +597,15 @@ public LongDeserializer(Class<Long> cls, Long nvl) {
@Override
public boolean isCachable() { return true; }

/**
* @since 3.1
*/
@Override
public ValueDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
{
return createRadixStringDeserializer(this, ctxt, property);
}

@Override
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
if (p.isExpectedNumberIntToken()) {
Expand Down Expand Up @@ -937,6 +976,15 @@ public final LogicalType logicalType() {
return LogicalType.Integer;
}

/**
* @since 3.1
*/
@Override
public ValueDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
{
return createRadixStringDeserializer(this, ctxt, property);
}

@Override
public BigInteger deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package tools.jackson.databind.deser.std;

import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;
import tools.jackson.databind.DeserializationContext;

import java.math.BigInteger;

/**
* Deserializer used for a string that represents a number in specific radix (base).
*
* @since 3.1
*/
public class FromStringWithRadixToNumberDeserializer
extends StdDeserializer<Number> {
private final int radix;

public FromStringWithRadixToNumberDeserializer(StdDeserializer<?> src, int radix) {
super(src);
this.radix = radix;
}

@Override
public Number deserialize(JsonParser p, DeserializationContext ctxt) {
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.getString();

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
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package tools.jackson.databind.deser.std;

import com.fasterxml.jackson.annotation.JsonFormat;
import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.DeserializationContext;

import static com.fasterxml.jackson.annotation.JsonFormat.DEFAULT_RADIX;

/**
* Factory class for {@link FromStringWithRadixToNumberDeserializer} for deserializers in {@link tools.jackson.databind.deser.jdk.NumberDeserializers}
* @since 3.1
*
*/
public class RadixSerializerCreator {
public static StdDeserializer<? extends Number> createRadixStringDeserializer(
StdScalarDeserializer<? extends Number> initialDeser,
DeserializationContext ctxt, BeanProperty property)
{
JsonFormat.Value format = initialDeser.findFormatOverrides(ctxt, property, initialDeser.handledType());

if (format == null || format.getShape() != JsonFormat.Shape.STRING) {
return initialDeser;
}

if (isSerializeWithRadixOverride(format)) {
int radix = format.getRadix();
return new FromStringWithRadixToNumberDeserializer(initialDeser, radix);
}

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) {
return format.hasNonDefaultRadix();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import tools.jackson.databind.util.ClassUtil;
import tools.jackson.databind.util.Converter;

import static com.fasterxml.jackson.annotation.JsonFormat.DEFAULT_RADIX;
Copy link
Member

Choose a reason for hiding this comment

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

Unnecessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed.


/**
* Base class for common deserializers. Contains shared
* base functionality for dealing with primitive values, such
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,28 @@ public JsonFormat.Value findFormatOverrides(MapperConfig<?> config) {
@Override
public JsonFormat.Value findPropertyFormat(MapperConfig<?> config, Class<?> baseType)
Copy link
Member

Choose a reason for hiding this comment

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

I am bit confused here -- why are changes needed? Shouldn't override handling work for radix just like all other properties? If not, why not (are fixes needed in jackson-annotations?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason is to consider default format out of ConfigOverrides which has the lowest precedence. This way we can make the a given radix apply to all integral types. This is what you meant here, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I also added a test for this behavior DifferentRadixNumberFormatTest#testIntSerializedAsHexStringWithDefaultRadix

{
JsonFormat.Value v0 = EMPTY_FORMAT.withRadix(config.getDefaultRadix());
JsonFormat.Value v1 = config.getDefaultPropertyFormat(baseType);
JsonFormat.Value v2 = findFormatOverrides(config);
if (v1 == null) {
return (v2 == null) ? EMPTY_FORMAT : v2;
JsonFormat.Value v2 = null;
AnnotationIntrospector intr = config.getAnnotationIntrospector();
if (intr != null) {
AnnotatedMember member = getMember();
Copy link
Member

@cowtowncoder cowtowncoder Dec 4, 2025

Choose a reason for hiding this comment

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

Why are we mixing in member annotations now -- this changes semantics of the method.

EDIT: never mind, that was done via findFormatOverrides() which does about same. But maybe call that method instead of inlining the logic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Do not remember why I inlined in #5317

if (member != null) {
v2 = intr.findFormat(config, member);
}
}

JsonFormat.Value formatValue = EMPTY_FORMAT;
if (v0 != null) {
Copy link
Member

Choose a reason for hiding this comment

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

How could this be null when it is assigned on line 61?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

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
Expand Down
Loading