Skip to content

Commit 6735de9

Browse files
Add field-level amount representation for Joda-Money (@JodaMoney annotation) (#76)
1 parent bae163a commit 6735de9

File tree

9 files changed

+808
-2
lines changed

9 files changed

+808
-2
lines changed

joda-money/src/main/java/tools/jackson/datatype/jodamoney/AmountRepresentation.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66
*/
77
public enum AmountRepresentation {
88

9+
/**
10+
* Default representation (inherit module-level configuration).
11+
* When used in field-level annotation, indicates that the field should use
12+
* the module's default representation.
13+
*
14+
*
15+
* @since 3.1
16+
*/
17+
DEFAULT,
18+
919
/**
1020
* Decimal number representation, where amount is (de)serialized as decimal number equal
1121
* to {@link org.joda.money.Money Money}'s amount, e.g. {@code 12.34} for
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package tools.jackson.datatype.jodamoney;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
import com.fasterxml.jackson.annotation.JacksonAnnotation;
9+
10+
/**
11+
* Annotation for configuring serialization and deserialization of {@link org.joda.money.Money Money}
12+
* at the field level. This annotation allows per-property override of the amount representation
13+
* used when converting Money to/from JSON.
14+
* <p>
15+
* When applied to a field, getter, or constructor parameter, this annotation takes precedence over
16+
* the module-level configuration set via {@link JodaMoneyModule#withAmountRepresentation(AmountRepresentation)}.
17+
* <p>
18+
* Example usage:
19+
* <pre>
20+
* public class Payment {
21+
* &#64;JodaMoney(amountRepresentation = AmountRepresentation.DECIMAL_STRING)
22+
* private Money amount;
23+
*
24+
* &#64;JodaMoney(amountRepresentation = AmountRepresentation.MINOR_CURRENCY_UNIT)
25+
* private Money fee;
26+
* }
27+
* </pre>
28+
*
29+
* @see AmountRepresentation
30+
* @see JodaMoneyModule#withAmountRepresentation(AmountRepresentation)
31+
*
32+
* @since 3.1
33+
*/
34+
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
35+
@Retention(RetentionPolicy.RUNTIME)
36+
@JacksonAnnotation
37+
public @interface JodaMoney {
38+
39+
/**
40+
* Specifies the amount representation to use for this property.
41+
* <p>
42+
* Defaults to {@link AmountRepresentation#DEFAULT}, which means the property
43+
* will use the module-level configuration or the built-in default
44+
* ({@link AmountRepresentation#DECIMAL_NUMBER}).
45+
*
46+
* @return the amount representation to use
47+
*/
48+
AmountRepresentation amountRepresentation() default AmountRepresentation.DEFAULT;
49+
}

joda-money/src/main/java/tools/jackson/datatype/jodamoney/JodaMoneyModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public void setupModule(SetupContext context)
5353

5454
public JodaMoneyModule withAmountRepresentation(final AmountRepresentation representation) {
5555
switch (representation) {
56+
case DEFAULT:
5657
case DECIMAL_NUMBER:
5758
return new JodaMoneyModule(DecimalNumberAmountConverter.getInstance());
5859
case DECIMAL_STRING:

joda-money/src/main/java/tools/jackson/datatype/jodamoney/MoneyDeserializer.java

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,14 @@
44
import java.util.Arrays;
55
import java.util.Collection;
66

7+
import com.fasterxml.jackson.annotation.JsonFormat;
78
import tools.jackson.core.JacksonException;
89
import tools.jackson.core.JsonParser;
910
import tools.jackson.core.JsonToken;
1011

12+
import tools.jackson.databind.BeanProperty;
1113
import tools.jackson.databind.DeserializationContext;
14+
import tools.jackson.databind.ValueDeserializer;
1215
import tools.jackson.databind.deser.std.StdDeserializer;
1316
import tools.jackson.databind.jsontype.TypeDeserializer;
1417
import tools.jackson.databind.type.LogicalType;
@@ -20,8 +23,8 @@
2023

2124
public class MoneyDeserializer extends StdDeserializer<Money>
2225
{
23-
private final String F_AMOUNT = "amount";
24-
private final String F_CURRENCY = "currency";
26+
private static final String F_AMOUNT = "amount";
27+
private static final String F_CURRENCY = "currency";
2528
private final AmountConverter amountConverter;
2629

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

41+
@Override
42+
public ValueDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
43+
if (property == null) {
44+
return this;
45+
}
46+
47+
AmountRepresentation effectiveRepresentation = _resolveRepresentation(property, ctxt);
48+
49+
if (effectiveRepresentation == null || effectiveRepresentation == AmountRepresentation.DEFAULT) {
50+
// Keep current converter (module-level default)
51+
return this;
52+
}
53+
54+
AmountConverter newConverter = _getConverterForRepresentation(effectiveRepresentation);
55+
if (newConverter == this.amountConverter) {
56+
return this;
57+
}
58+
59+
return new MoneyDeserializer(newConverter);
60+
}
61+
62+
private AmountRepresentation _resolveRepresentation(BeanProperty property, DeserializationContext ctxt) {
63+
// Priority 1: @JodaMoney annotation
64+
JodaMoney jodaMoney = property.getAnnotation(JodaMoney.class);
65+
if (jodaMoney != null && jodaMoney.amountRepresentation() != AmountRepresentation.DEFAULT) {
66+
return jodaMoney.amountRepresentation();
67+
}
68+
69+
// Priority 2: @JsonFormat mapping
70+
JsonFormat.Value format = property.findPropertyFormat(ctxt.getConfig(), Money.class);
71+
if (format != null && format.getShape() != JsonFormat.Shape.ANY) {
72+
AmountRepresentation mapped = _mapShapeToRepresentation(format.getShape());
73+
if (mapped != null) {
74+
return mapped;
75+
}
76+
}
77+
78+
// Priority 3 & 4: Module default or built-in default (already in amountConverter)
79+
return null;
80+
}
81+
82+
private AmountRepresentation _mapShapeToRepresentation(JsonFormat.Shape shape) {
83+
switch (shape) {
84+
case STRING:
85+
return AmountRepresentation.DECIMAL_STRING;
86+
case NUMBER:
87+
case NUMBER_FLOAT:
88+
return AmountRepresentation.DECIMAL_NUMBER;
89+
case NUMBER_INT:
90+
return AmountRepresentation.MINOR_CURRENCY_UNIT;
91+
default:
92+
return null; // Ignore other shapes
93+
}
94+
}
95+
96+
private AmountConverter _getConverterForRepresentation(AmountRepresentation representation) {
97+
switch (representation) {
98+
case DECIMAL_NUMBER:
99+
return DecimalNumberAmountConverter.getInstance();
100+
case DECIMAL_STRING:
101+
return DecimalStringAmountConverter.getInstance();
102+
case MINOR_CURRENCY_UNIT:
103+
return MinorCurrencyUnitAmountConverter.getInstance();
104+
default:
105+
return this.amountConverter;
106+
}
107+
}
108+
38109
@Override
39110
public LogicalType logicalType() {
40111
// structured, hence POJO

joda-money/src/main/java/tools/jackson/datatype/jodamoney/MoneySerializer.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,13 @@
22

33
import org.joda.money.Money;
44

5+
import com.fasterxml.jackson.annotation.JsonFormat;
56
import tools.jackson.core.JacksonException;
67
import tools.jackson.core.JsonGenerator;
78
import tools.jackson.core.JsonToken;
89
import tools.jackson.core.type.WritableTypeId;
10+
import tools.jackson.databind.BeanProperty;
11+
import tools.jackson.databind.ValueSerializer;
912
import tools.jackson.databind.SerializationContext;
1013
import tools.jackson.databind.jsontype.TypeSerializer;
1114

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

31+
@Override
32+
public ValueSerializer<?> createContextual(SerializationContext ctxt, BeanProperty property) {
33+
if (property == null) {
34+
return this;
35+
}
36+
37+
AmountRepresentation effectiveRepresentation = _resolveRepresentation(property, ctxt);
38+
39+
if (effectiveRepresentation == null || effectiveRepresentation == AmountRepresentation.DEFAULT) {
40+
// Keep current converter (module-level default)
41+
return this;
42+
}
43+
44+
AmountConverter newConverter = _getConverterForRepresentation(effectiveRepresentation);
45+
if (newConverter == this.amountConverter) {
46+
return this;
47+
}
48+
49+
return new MoneySerializer(newConverter);
50+
}
51+
52+
private AmountRepresentation _resolveRepresentation(BeanProperty property, SerializationContext ctxt) {
53+
// Priority 1: @JodaMoney annotation
54+
JodaMoney jodaMoney = property.getAnnotation(JodaMoney.class);
55+
if (jodaMoney != null && jodaMoney.amountRepresentation() != AmountRepresentation.DEFAULT) {
56+
return jodaMoney.amountRepresentation();
57+
}
58+
59+
// Priority 2: @JsonFormat mapping
60+
JsonFormat.Value format = property.findPropertyFormat(ctxt.getConfig(), Money.class);
61+
if (format != null && format.getShape() != JsonFormat.Shape.ANY) {
62+
AmountRepresentation mapped = _mapShapeToRepresentation(format.getShape());
63+
if (mapped != null) {
64+
return mapped;
65+
}
66+
}
67+
68+
// Priority 3 & 4: Module default or built-in default (already in amountConverter)
69+
return null;
70+
}
71+
72+
private AmountRepresentation _mapShapeToRepresentation(JsonFormat.Shape shape) {
73+
switch (shape) {
74+
case STRING:
75+
return AmountRepresentation.DECIMAL_STRING;
76+
case NUMBER:
77+
case NUMBER_FLOAT:
78+
return AmountRepresentation.DECIMAL_NUMBER;
79+
case NUMBER_INT:
80+
return AmountRepresentation.MINOR_CURRENCY_UNIT;
81+
default:
82+
return null; // Ignore other shapes
83+
}
84+
}
85+
86+
private AmountConverter _getConverterForRepresentation(AmountRepresentation representation) {
87+
switch (representation) {
88+
case DECIMAL_NUMBER:
89+
return DecimalNumberAmountConverter.getInstance();
90+
case DECIMAL_STRING:
91+
return DecimalStringAmountConverter.getInstance();
92+
case MINOR_CURRENCY_UNIT:
93+
return MinorCurrencyUnitAmountConverter.getInstance();
94+
default:
95+
return this.amountConverter;
96+
}
97+
}
98+
2899
@Override
29100
public void serialize(final Money value,
30101
final JsonGenerator g, final SerializationContext ctxt)

0 commit comments

Comments
 (0)