Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
*/
public enum AmountRepresentation {

/**
* Default representation (inherit module-level configuration).
* When used in field-level annotation, indicates that the field should use
* the module's default representation.
*/
DEFAULT,

/**
* Decimal number representation, where amount is (de)serialized as decimal number equal
* to {@link org.joda.money.Money Money}'s amount, e.g. {@code 12.34} for
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package tools.jackson.datatype.jodamoney;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import com.fasterxml.jackson.annotation.JacksonAnnotation;

/**
* Annotation for configuring serialization and deserialization of {@link org.joda.money.Money Money}
* at the field level. This annotation allows per-property override of the amount representation
* used when converting Money to/from JSON.
* <p>
* When applied to a field, getter, or constructor parameter, this annotation takes precedence over
* the module-level configuration set via {@link JodaMoneyModule#withAmountRepresentation(AmountRepresentation)}.
* <p>
* Example usage:
* <pre>
* public class Payment {
* &#64;JodaMoney(amountRepresentation = AmountRepresentation.DECIMAL_STRING)
* private Money amount;
*
* &#64;JodaMoney(amountRepresentation = AmountRepresentation.MINOR_CURRENCY_UNIT)
* private Money fee;
* }
* </pre>
*
* @see AmountRepresentation
* @see JodaMoneyModule#withAmountRepresentation(AmountRepresentation)
*/
Copy link
Member

Choose a reason for hiding this comment

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

@SInCE 3.1

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@JacksonAnnotation
public @interface JodaMoney {

/**
* Specifies the amount representation to use for this property.
* <p>
* Defaults to {@link AmountRepresentation#DEFAULT}, which means the property
* will use the module-level configuration or the built-in default
* ({@link AmountRepresentation#DECIMAL_NUMBER}).
*
* @return the amount representation to use
*/
AmountRepresentation amountRepresentation() default AmountRepresentation.DEFAULT;
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public void setupModule(SetupContext context)

public JodaMoneyModule withAmountRepresentation(final AmountRepresentation representation) {
switch (representation) {
case DEFAULT:
case DECIMAL_NUMBER:
return new JodaMoneyModule(DecimalNumberAmountConverter.getInstance());
case DECIMAL_STRING:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@
import java.util.Arrays;
import java.util.Collection;

import com.fasterxml.jackson.annotation.JsonFormat;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonParser;
import tools.jackson.core.JsonToken;

import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.DeserializationContext;
import tools.jackson.databind.ValueDeserializer;
import tools.jackson.databind.deser.std.StdDeserializer;
import tools.jackson.databind.jsontype.TypeDeserializer;
import tools.jackson.databind.type.LogicalType;
Expand All @@ -20,8 +23,8 @@

public class MoneyDeserializer extends StdDeserializer<Money>
{
private final String F_AMOUNT = "amount";
private final String F_CURRENCY = "currency";
private static final String F_AMOUNT = "amount";
private static final String F_CURRENCY = "currency";
private final AmountConverter amountConverter;

// Kept to maintain backward compatibility with 2.x
Expand All @@ -35,6 +38,74 @@ public MoneyDeserializer() {
this.amountConverter = requireNonNull(amountConverter, "amount converter cannot be null");
}

@Override
public ValueDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
if (property == null) {
return this;
}

AmountRepresentation effectiveRepresentation = _resolveRepresentation(property, ctxt);

if (effectiveRepresentation == null || effectiveRepresentation == AmountRepresentation.DEFAULT) {
// Keep current converter (module-level default)
return this;
}

AmountConverter newConverter = _getConverterForRepresentation(effectiveRepresentation);
if (newConverter == this.amountConverter) {
return this;
}

return new MoneyDeserializer(newConverter);
}

private AmountRepresentation _resolveRepresentation(BeanProperty property, DeserializationContext ctxt) {
// Priority 1: @JodaMoney annotation
JodaMoney jodaMoney = property.getAnnotation(JodaMoney.class);
if (jodaMoney != null && jodaMoney.amountRepresentation() != AmountRepresentation.DEFAULT) {
return jodaMoney.amountRepresentation();
}

// Priority 2: @JsonFormat mapping
JsonFormat.Value format = property.findPropertyFormat(ctxt.getConfig(), Money.class);
if (format != null && format.getShape() != JsonFormat.Shape.ANY) {
AmountRepresentation mapped = _mapShapeToRepresentation(format.getShape());
if (mapped != null) {
return mapped;
}
}

// Priority 3 & 4: Module default or built-in default (already in amountConverter)
return null;
}

private AmountRepresentation _mapShapeToRepresentation(JsonFormat.Shape shape) {
switch (shape) {
case STRING:
return AmountRepresentation.DECIMAL_STRING;
case NUMBER:
case NUMBER_FLOAT:
return AmountRepresentation.DECIMAL_NUMBER;
case NUMBER_INT:
return AmountRepresentation.MINOR_CURRENCY_UNIT;
default:
return null; // Ignore other shapes
}
}

private AmountConverter _getConverterForRepresentation(AmountRepresentation representation) {
switch (representation) {
case DECIMAL_NUMBER:
return DecimalNumberAmountConverter.getInstance();
case DECIMAL_STRING:
return DecimalStringAmountConverter.getInstance();
case MINOR_CURRENCY_UNIT:
return MinorCurrencyUnitAmountConverter.getInstance();
default:
return this.amountConverter;
}
}

@Override
public LogicalType logicalType() {
// structured, hence POJO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import org.joda.money.Money;

import com.fasterxml.jackson.annotation.JsonFormat;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.core.JsonToken;
import tools.jackson.core.type.WritableTypeId;
import tools.jackson.databind.BeanProperty;
import tools.jackson.databind.ValueSerializer;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.jsontype.TypeSerializer;

Expand All @@ -25,6 +28,74 @@ public MoneySerializer() {
this.amountConverter = requireNonNull(amountConverter, "amount converter cannot be null");
}

@Override
public ValueSerializer<?> createContextual(SerializationContext ctxt, BeanProperty property) {
if (property == null) {
return this;
}

AmountRepresentation effectiveRepresentation = _resolveRepresentation(property, ctxt);

if (effectiveRepresentation == null || effectiveRepresentation == AmountRepresentation.DEFAULT) {
// Keep current converter (module-level default)
return this;
}

AmountConverter newConverter = _getConverterForRepresentation(effectiveRepresentation);
if (newConverter == this.amountConverter) {
return this;
}

return new MoneySerializer(newConverter);
}

private AmountRepresentation _resolveRepresentation(BeanProperty property, SerializationContext ctxt) {
// Priority 1: @JodaMoney annotation
JodaMoney jodaMoney = property.getAnnotation(JodaMoney.class);
if (jodaMoney != null && jodaMoney.amountRepresentation() != AmountRepresentation.DEFAULT) {
return jodaMoney.amountRepresentation();
}

// Priority 2: @JsonFormat mapping
JsonFormat.Value format = property.findPropertyFormat(ctxt.getConfig(), Money.class);
if (format != null && format.getShape() != JsonFormat.Shape.ANY) {
AmountRepresentation mapped = _mapShapeToRepresentation(format.getShape());
if (mapped != null) {
return mapped;
}
}

// Priority 3 & 4: Module default or built-in default (already in amountConverter)
return null;
}

private AmountRepresentation _mapShapeToRepresentation(JsonFormat.Shape shape) {
switch (shape) {
case STRING:
return AmountRepresentation.DECIMAL_STRING;
case NUMBER:
case NUMBER_FLOAT:
return AmountRepresentation.DECIMAL_NUMBER;
case NUMBER_INT:
return AmountRepresentation.MINOR_CURRENCY_UNIT;
default:
return null; // Ignore other shapes
}
}

private AmountConverter _getConverterForRepresentation(AmountRepresentation representation) {
switch (representation) {
case DECIMAL_NUMBER:
return DecimalNumberAmountConverter.getInstance();
case DECIMAL_STRING:
return DecimalStringAmountConverter.getInstance();
case MINOR_CURRENCY_UNIT:
return MinorCurrencyUnitAmountConverter.getInstance();
default:
return this.amountConverter;
}
}

@Override
public void serialize(final Money value,
final JsonGenerator g, final SerializationContext ctxt)
Expand Down
Loading