Skip to content

Add configurable amount representations for Joda Money module #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Conversation

apankowski
Copy link
Contributor

@apankowski apankowski commented Oct 3, 2021

This PR adds the possibility to configure desired representation of (de)serialized Money instances to the Joda Money Jackson module.

It allows selection of one of 3 representations via the JodaMoneyModule.withAmountRepresentation(AmountRepresentation) method:

  • DECIMAL_NUMBER - the default; amounts are (de)serialized as decimal numbers equal to the monetary amount, e.g. 12.34 for EUR 12.34,
  • DECIMAL_STRING - amounts are (de)serialized as strings containing decimal number equal to the monetary amount, e.g. "12.34" for EUR 12.34,
  • MINOR_CURRENCY_UNIT - amounts are (de)serialized as long integers equal to the monetary amount expressed in minor currency unit, e.g. 1234 for EUR 12.34, 12345 for KWD 12.345 or 12 for JPY 12.

The motivation for having such feature is that:

  1. depending on the client implementation, passing monetary amounts as decimal numbers poses a risk of involving floating-point number representation, which may lead to loss of precision & rounding errors, especially in cases when some form of manipulation or arithmetic on the amounts is necessary,
  2. some APIs as well as quite popular frontend monetary amount handling library Dinero.js require providing monetary amounts in minor currency units,
  3. there are numerous APIs that use the decimal string (quoted decimal numbers) representation.

Noteworthy points:

  1. I tried to align with the existing code style, but may have missed something.
  2. I took the liberty of using parametrized tests with help of JUnitParams.
  3. I tried to be careful to design the public parts of the new code to not expose too much and allow extensibility, but it would be good to review it thoroughly.
  4. I believe the changes don't change the existing API in any breaking way nor do they change the existing behavior.
  5. I have branched off of 2.13 (since it was the default) and made changes to code on that branch - not sure if that's correct.
  6. I will leave comments for discussion wherever I'm not sure of something.
  7. I'm not sure about the minimal JDK version we're targeting. I assumed JDK 8+ and therefore used Objects.requireNonNull and lambdas.

I will be helpful for pointers, suggestions and remarks and will try to align with all of them 🙂

@cowtowncoder cowtowncoder added the 2.14 Intended for version 2.14.x label Oct 3, 2021

public interface AmountRepresenter<T> {

T write(Money money);
Copy link
Member

Choose a reason for hiding this comment

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

Either name is sub-optimal (nothing gets written), or (more likely I think) this should do actual writing via JsonGenerator directly. If former, I think this is something like AmountConverter, and methods could be something like fromMoney() and toMoney().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, I struggled with the name so it's definitely sub-optimal. your proposal is great, I will use it, thanks

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good!

g.writeNumberField("amount", decimal.setScale(scale, RoundingMode.UNNECESSARY));
g.writeFieldName("currency");
context.defaultSerializeValue(money.getCurrencyUnit(), g);
g.writeObjectField("amount", amountRepresenter.write(money));
Copy link
Member

Choose a reason for hiding this comment

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

But doesn't this always chnge value from JSON Number into JSON String? And be essentially backwards-incompatible change?

This is why I think instead of write() conversion method it should be method that does write.
So, first g.writeFieldName("amount"), then value write by helper object.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it isn't output as JSON string. it goes like this:

  • BigDecimal is serialized using NumberSerializer (which does JsonGenerator.writeNumber(BigDecimal) just as we would do directly),
  • String is serialized using StringSerializer,
  • Long - using NumberSerializers.LongSerializer.

this seems to be confirmed by both manual tests and automatic tests in MoneySerializerTest.java

Copy link
Member

Choose a reason for hiding this comment

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

Ah. I did miss the paramaterization, yes, Long and BigDecimal will then be written using configured serializer which is fine.

@cowtowncoder
Copy link
Member

First of all: thank you for contributing this patch! I haven't as much time to spend on things as I'd like, but I hope to provide some feedback.

I think the goals make sense, and I appreciate care taken. I did have some concerns (which may be due to my misreading?), added comments.

One practical thing is that due to changes to API, this would need to go in 2.14. I haven't yet created a branch for it, though, so for time being 2.13 makes most sense (master is for Jackson 3.0 that is further out).

Another thing is that more commonly formatting aspects would be handled via @JsonFormat annotation (esp. its shape), but I realize it may not be extensible enough here. It would allow differentiating between Number and String representation. It might work, since we do have:

  1. Shape.STRING
  2. Shape.NUMBER_FLOAT
  3. Shape.NUMBER_INT (for "minor")
  4. Shape.NUMBER would be ambiguous; I assume that'd be same as (2)

The main benefits would be that everything (?) would be configurable by type and/or property; you can annotate properties explicitly, or indicate default for given type.

So this would be something to consider if you have time. I can try to help fill in blanks on usage; there isn't much tutorial on how to do these unfortunately.

Copy link
Contributor Author

@apankowski apankowski left a comment

Choose a reason for hiding this comment

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

thank you for the pointers, I've answered the comments and pushed an update 🙂

regarding:

more commonly formatting aspects would be handled via @jsonformat annotation (esp. its shape), but I realize it may not be extensible enough here. It would allow differentiating between Number and String representation. It might work, since we do have:

  • Shape.STRING
  • Shape.NUMBER_FLOAT
  • Shape.NUMBER_INT (for "minor")
  • Shape.NUMBER would be ambiguous; I assume that'd be same as (2)

The main benefits would be that everything (?) would be configurable by type and/or property; you can annotate properties explicitly, or indicate default for given type.

I actually wanted to approach something similar in a follow-up PR (to keep them smaller), but I can do it in this one if you prefer.

what I wasn't sure was if it will be natural for users (or: won't they be surprised) that a Money field is always serialized as JSON object, but @JsonFormat(shape = ) on such field controls not serialization of the field itself, but just the "amount" field inside of it. (I guess we could just document it of course.)

second thing I was considering was a dedicated annotation, something like:

@JsonMoney(amountRepresentation = MINOR_CURRENCY_UNIT)
Money grandTotal;

but I'm not sure if reading such custom annotation is feasible in (de)serializers, won't break something (e.g. caching of (de)serializers), etc.

what do you think is the way to go in Jackson's world? 🙂


public interface AmountRepresenter<T> {

T write(Money money);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah, I struggled with the name so it's definitely sub-optimal. your proposal is great, I will use it, thanks

g.writeNumberField("amount", decimal.setScale(scale, RoundingMode.UNNECESSARY));
g.writeFieldName("currency");
context.defaultSerializeValue(money.getCurrencyUnit(), g);
g.writeObjectField("amount", amountRepresenter.write(money));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

it isn't output as JSON string. it goes like this:

  • BigDecimal is serialized using NumberSerializer (which does JsonGenerator.writeNumber(BigDecimal) just as we would do directly),
  • String is serialized using StringSerializer,
  • Long - using NumberSerializers.LongSerializer.

this seems to be confirmed by both manual tests and automatic tests in MoneySerializerTest.java

@apankowski
Copy link
Contributor Author

hey @cowtowncoder, friendly ping 🙂

would be great if you could state what you prefer as outlined in this comment. then I could align this PR with whatever we agree 🙂

@cowtowncoder
Copy link
Member

Yeah sorry have had bit of burnout wrt OSS work so feedback has been slow. But I do want to help here as this looks like a good improvement.

So: on @JsonFormat yeah... maybe that'd be unexpected; there is no precedent here.
I think going with the initial idea is fine; even if annotation support was added it could be incremental feature and not replacement (so module can define global defaults).

On getting this merged: with 2.13.0 this needs to go in 2.14; I haven't yet created 2.14 branches but hope to do that soon.

I'll add this on my TODO list and try to remember update once I get 2.14 branch set up for jackson-databind and this module.

@apankowski
Copy link
Contributor Author

no worries @cowtowncoder, I get it 🙂

let's wait for 2.14 and do let me know if you spot anything that I should change 👍

@cowtowncoder
Copy link
Member

2.14 branches exists for all repos now.

@apankowski apankowski force-pushed the feature/add-joda-money-amount-representations branch from b7c562a to d789f73 Compare October 24, 2021 08:45
@apankowski apankowski changed the base branch from 2.13 to 2.14 October 24, 2021 08:45
@apankowski
Copy link
Contributor Author

apankowski commented Oct 24, 2021

@cowtowncoder roger, rebased on top of 2.14, verified all tests pass and updated PR 👍

@cowtowncoder
Copy link
Member

Ok still being slow -- this is on top of my review list, hoping to get it merged this week.

@cowtowncoder
Copy link
Member

cowtowncoder commented Nov 24, 2021

@apankowski Apologies for yet more delays, but I'm back. I think things are good; only had couple of suggestions / questions:

  1. Javadocs would be good for couple of classes; I'll add a brief comment.
  2. ---Would it make sense to allow specific "withAmountConverter()" for fully custom translations; in addition to more convenient enum-alternative?--- (NO NEED)
  3. For backwards-compatibility may want to keep deprecated 0-args constructors for Money[De]Serializer.

These are fully optional; just LMK what you think and I can merge things as-is or if you want to add/change something let me know when things are ready.

Actually -- after thinking about this bit more, no, maybe NOT exposing AmountConverters initially is better.
That avoids issues wrt interface design etc.

It also reduces need to describe implementations: so just adding Javadocs to enum values would be sufficient.

@apankowski
Copy link
Contributor Author

@cowtowncoder no worries - thank you for the feedback 🙂

Will apply all agreed changes and let you know soon. I could use some guidance with visibility of the AmountConverters:

  1. leave as is: AmountConverter and implementations are public (we document them a bit since they become part of the contract)
  2. leave (documented) AmountConverter interface public but keep specific implementations package-private
  3. make AmountConverter and specific implementations package-private

I can't decide how much we'd like to make public at first and could use your experience here

@cowtowncoder
Copy link
Member

Good questions! Since we do not really expose any way to use them except via enums, my slight preference would be for (3). That leaves us freedom to change things more should there be need for more customizations, in ways that we haven't anticipated.

@apankowski
Copy link
Contributor Author

@cowtowncoder PR updated and ready for next round of feedback 🙂

Summary of changes:

  • Added module registration code sample in REAMDE.
  • Removed generics from AmountConverter.
  • Reduced visibility of the amount representation-related classes to package-private except AmountRepresentation enum and corresponding JodaMoneyModule.withAmountRepresentation() representation selection method.
  • Brought back default public constructors for Money(De)Serializers to maintain compatibility with existing public API. Didn't add the @Deprecated annotation on them since API clients won't have another option to construct the (de)serializers directly (if they do it with existing API). The only other (indirect) way is by registering the JodaMoneyModule. Do you think we should add @Deprecated on default constructors of the (de)serializers and point clients to registering the JodaMoneyModule instead?
  • Added simple (but hopefully readable) JavaDocs to amount representation-related classes.

@cowtowncoder cowtowncoder added this to the 2.14.0 milestone Nov 30, 2021
@cowtowncoder
Copy link
Member

Looks good! I'll try to add some @since annotations but that can be done post-merge.

@cowtowncoder cowtowncoder merged commit d9e0397 into FasterXML:2.14 Nov 30, 2021
@cowtowncoder cowtowncoder changed the title Feature: Add configurable amount representations for Joda Money module Add configurable amount representations for Joda Money module Nov 30, 2021
cowtowncoder added a commit that referenced this pull request Nov 30, 2021
@cowtowncoder
Copy link
Member

Thank you again @apankowski !

@apankowski apankowski deleted the feature/add-joda-money-amount-representations branch August 20, 2022 13:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
2.14 Intended for version 2.14.x
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants