Search code examples
jsonserializationjakarta-eejax-rsjava-money

Custom Json serialization format for MonetaryAmount JSR354 / moneta, how to register the Serializer properly


I have the following InvoiceItem class which has monetary amounts:

import jakarta.json.bind.annotation.JsonbProperty;
import jakarta.json.bind.annotation.JsonbTypeSerializer;
import jakarta.json.bind.serializer.JsonbSerializer;
import jakarta.json.bind.serializer.SerializationContext;
import jakarta.json.stream.JsonGenerator;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import org.eclipse.persistence.annotations.UuidGenerator;
import org.javamoney.moneta.FastMoney;

@Entity
@UuidGenerator(name = "INVOICE_ITEM_GEN")
@Table(name = "invoice_items")
public class InvoiceItem {
    @Getter
    @Setter
    @jakarta.persistence.Id
    @JsonbProperty("id")
    @GeneratedValue(generator = "INVOICE_ITEM_GEN")
    private String Id;

    @Getter
    @Setter
    @Column(name = "name")
    @JsonbProperty("name")
    private String Name;

    @Getter
    @Setter
    @Convert(converter = PersistentFastMoney.class)
    @Column(name = "price")
    @JsonbProperty("price")
    private FastMoney Price;

    public InvoiceItem() {
    }

    public InvoiceItem(String name, String price) {
        this.setName(name);
        this.setPrice(FastMoney.parse(price));
    }
}

I generate this successfully in my function from a different input.

import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/invoices")
public class InvoiceSystem {

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Invoice CreateNewInvoice(InvoiceInput input) {
        var invoice = new Invoice();
        invoice.setCreditor(new InvoiceParty());
        for (var item: input.getItems()) {
            invoice.Items.add(item.toInvoiceItem());
        }

        return invoice;
    }
}

The toInvoiceItem uses the constructor of InvoiceItem with two string parameters.

Now I want to print the class as JSON back to the consumer after it has been created.

But this fails with:

<body><h1>HTTP Status 500 - Internal Server Error</h1>
<hr/>
<p><b>type</b> Exception report</p>
<p><b>message</b>Internal Server Error</p>
<p><b>description</b>The server encountered an internal error that prevented it from fulfilling this request.</p>
<p><b>exception</b>
<pre>javax.servlet.ServletException: javax.json.bind.JsonbException: Unable to serialize property &#39;Items&#39; from com.openflowlabs.faktura.Invoice</pre>
</p><p><b>root cause</b>
<pre>javax.json.bind.JsonbException: Unable to serialize property &#39;Items&#39; from com.openflowlabs.faktura.Invoice</pre>
</p><p><b>root cause</b>
<pre>javax.json.bind.JsonbException: Unable to serialize property &#39;price&#39; from com.openflowlabs.faktura.InvoiceItem</pre>
</p><p><b>root cause</b>
<pre>javax.json.bind.JsonbException: Unable to serialize property &#39;context&#39; from org.javamoney.moneta.FastMoney</pre>
</p><p><b>root cause</b>
<pre>javax.json.bind.JsonbException: Unable to serialize property &#39;amountType&#39; from javax.money.MonetaryContext</pre>
</p><p><b>root cause</b>
<pre>javax.json.bind.JsonbException: Unable to serialize property &#39;annotatedInterfaces&#39; from java.lang.Class</pre>
</p><p><b>root cause</b>
<pre>javax.json.bind.JsonbException: Unable to serialize property &#39;annotatedOwnerType&#39; from sun.reflect.annotation.AnnotatedTypeFactory.AnnotatedTypeBaseImpl</pre>
</p><p><b>root cause</b>
<pre>javax.json.bind.JsonbException: Error getting value on: javax.money.MonetaryAmount</pre>
</p><p><b>root cause</b>
<pre>java.lang.IllegalAccessException: class org.eclipse.yasson.internal.model.ReflectionPropagation cannot access class sun.reflect.annotation.AnnotatedTypeFactory$AnnotatedTypeBaseImpl &#40;in module java.base&#41; because module java.base does not export sun.reflect.annotation to unnamed module @50c1e8e3</pre>
</p><p><b>note</b> <u>The full stack traces of the exception and its root causes are available in the Payara Server
    5.2022.1 #badassfish logs.</u></p>
<hr/>
<h3>Payara Server 5.2022.1 #badassfish</h3></body>

I know I need a custom serializer, but I tried for a day every possible way to make one but my app simply won't eat the annotation.

How do I make a custom serializer for the org.javamoney.moneta.FastMoney class so that I can simply annotate all fields that have this class with @JsonbTypeSerializer(FastMoneySerializer.class) like for example Invoice Item

@Getter
@Setter
@Convert(converter = PersistentFastMoney.class)
@Column(name = "price")
@JsonbProperty("price")
@JsonbTypeSerializer(FastMoneySerializer.class)
private FastMoney Price;

Is this possible somehow? Is there some documentation on how I would need to make the serializer so it returns the JSON like this?

{
  "name": "Testing",
  "price": "CHF 20"
}

Thanks for any pointers and or solutions I am really stuck and can't find a tutorial on how to make these serializers work. If you have a library with good documentation that would also help.


Solution

  • OK, I found a solution.

    If you want to use jakarta.json your runtime needs to support it. I was using Pyara which says it supports Jakarta EE9 but somehow it does not. Switching to GlassFish solved that problem. The Correct Serializer was then done like this

    package org.mypackage;
    
    import jakarta.json.bind.serializer.JsonbSerializer;
    import jakarta.json.bind.serializer.SerializationContext;
    import jakarta.json.stream.JsonGenerator;
    import org.javamoney.moneta.FastMoney;
    
    public class FastMoneySerializer implements JsonbSerializer<FastMoney> {
    
        @Override
        public void serialize(FastMoney obj, JsonGenerator generator, SerializationContext ctx) {
            var str = obj.toString();
            generator.write(str);
        }
    }
    

    You can then add it to a property that has the FastMoney type like so:

    package com.openflowlabs.faktura;
    
    import jakarta.json.bind.annotation.JsonbAnnotation;
    import jakarta.json.bind.annotation.JsonbProperty;
    import jakarta.json.bind.annotation.JsonbTypeSerializer;
    import jakarta.json.bind.serializer.JsonbSerializer;
    import jakarta.json.bind.serializer.SerializationContext;
    import jakarta.json.stream.JsonGenerator;
    import jakarta.persistence.*;
    import lombok.Getter;
    import lombok.Setter;
    import org.eclipse.persistence.annotations.UuidGenerator;
    import org.javamoney.moneta.FastMoney;
    
    @Entity
    @UuidGenerator(name = "INVOICE_ITEM_GEN")
    @Table(name = "invoice_items")
    public class InvoiceItem {
        @Getter
        @Setter
        @jakarta.persistence.Id
        @JsonbProperty("id")
        @GeneratedValue(generator = "INVOICE_ITEM_GEN")
        private String Id;
    
        @Getter
        @Setter
        @Column(name = "name")
        @JsonbProperty("name")
        private String Name;
    
        @Getter
        @Setter
        @Convert(converter = PersistentFastMoney.class)
        @Column(name = "price")
        @JsonbProperty("price")
        @JsonbTypeSerializer(FastMoneySerializer.class)
        private FastMoney Price;
    
        public InvoiceItem() {
        }
    
        public InvoiceItem(String name, String price) {
            this.setName(name);
            this.setPrice(FastMoney.parse(price));
        }
    }