eclipse-ee4j / yasson

Eclipse Yasson project
https://projects.eclipse.org/projects/ee4j.yasson
Other
204 stars 96 forks source link

yasson should support (de)serializing of "java.math.MathContext" #571

Open nimo23 opened 2 years ago

nimo23 commented 2 years ago

Describe the bug I get this error when trying to create json output of an instance of java.math.MathContext.

Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field final java.math.RoundingMode java.math.MathContext.roundingMode accessible: module java.base does not "opens java.math" to unnamed module @4dca0ecd
    at java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) ~[?:?]
    at java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) ~[?:?]
    at java.lang.reflect.Field.checkCanSetAccessible(Field.java:178) ~[?:?]
    at java.lang.reflect.Field.setAccessible(Field.java:172) ~[?:?]
    at org.eclipse.yasson.internal.model.PropertyValuePropagation.lambda$overrideAccessible$2(PropertyValuePropagation.java:153) ~[yasson-1.0.9.jar:?]
    at java.security.AccessController.doPrivileged(AccessController.java:318) ~[?:?]
    at org.eclipse.yasson.internal.model.PropertyValuePropagation.overrideAccessible(PropertyValuePropagation.java:152) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.model.PropertyValuePropagation.isFieldVisible(PropertyValuePropagation.java:127) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.model.PropertyValuePropagation.initReadable(PropertyValuePropagation.java:94) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.model.PropertyValuePropagation.<init>(PropertyValuePropagation.java:80) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.model.ReflectionPropagation.<init>(ReflectionPropagation.java:39) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.model.PropertyModel.<init>(PropertyModel.java:134) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.ClassParser.lambda$parseProperties$0(ClassParser.java:71) ~[yasson-1.0.9.jar:?]
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197) ~[?:?]
    at java.util.HashMap$ValueSpliterator.forEachRemaining(HashMap.java:1779) ~[?:?]
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509) ~[?:?]
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[?:?]
    at java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921) ~[?:?]
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[?:?]
    at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682) ~[?:?]
    at org.eclipse.yasson.internal.ClassParser.parseProperties(ClassParser.java:72) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.MappingContext.lambda$createParseClassModelFunction$1(MappingContext.java:97) ~[yasson-1.0.9.jar:?]
    at java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708) ~[?:?]
    at org.eclipse.yasson.internal.MappingContext.getOrCreateClassModel(MappingContext.java:81) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.ObjectSerializer.serializeInternal(ObjectSerializer.java:65) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.AbstractContainerSerializer.serialize(AbstractContainerSerializer.java:107) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.AbstractContainerSerializer.serializerCaptor(AbstractContainerSerializer.java:125) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.ObjectSerializer.marshallProperty(ObjectSerializer.java:121) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.ObjectSerializer.serializeInternal(ObjectSerializer.java:69) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.AbstractContainerSerializer.serialize(AbstractContainerSerializer.java:107) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.AbstractContainerSerializer.serializerCaptor(AbstractContainerSerializer.java:125) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.ObjectSerializer.marshallProperty(ObjectSerializer.java:121) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.ObjectSerializer.serializeInternal(ObjectSerializer.java:69) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.AbstractContainerSerializer.serialize(AbstractContainerSerializer.java:107) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.AbstractContainerSerializer.serializerCaptor(AbstractContainerSerializer.java:125) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.AbstractContainerSerializer.serializeItem(AbstractContainerSerializer.java:194) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.MapToObjectSerializer.serializeContainer(MapToObjectSerializer.java:99) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.MapSerializer.serializeInternal(MapSerializer.java:156) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.MapSerializer.serializeInternal(MapSerializer.java:27) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.AbstractContainerSerializer.serialize(AbstractContainerSerializer.java:107) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.AbstractContainerSerializer.serializerCaptor(AbstractContainerSerializer.java:125) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.ObjectSerializer.marshallProperty(ObjectSerializer.java:121) ~[yasson-1.0.9.jar:?]
    at org.eclipse.yasson.internal.serializer.ObjectSerializer.serializeInternal(ObjectSerializer.java:69) ~[yasson-1.0.9.jar:?]
    ... 79 more

To Reproduce Steps to reproduce the bug

Expected behavior To be able to (de)serialize java.math.MathContext.

System information:

jbescos commented 2 years ago

Could you provide a reproducer please?.

nimo23 commented 2 years ago

@jbescos Here is the test case leading to this exception:

import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbConfig;
import javax.json.bind.config.PropertyVisibilityStrategy;

import org.junit.jupiter.api.Test;

class BigDecimalJsonTest {

    public static class NumberText {

        private final BigDecimal number;
        private final MathContext mathContext;

        public NumberText(BigDecimal number, MathContext mathContext) {
            this.number = number;
            this.mathContext = mathContext;
        }

        public BigDecimal getNumber() {
            return number;
        }

        public MathContext getMathContext() {
            return mathContext;
        }
    }

    public static final class PrivateVisibilityStrategy implements PropertyVisibilityStrategy {
        @Override
        public boolean isVisible(Field field) {
            return true;
        }

        @Override
        public boolean isVisible(Method method) {
            return method.isAnnotationPresent(javax.json.bind.annotation.JsonbProperty.class);
        }
    }

    @Test
    final void testNumberText() {
        var mathContext = new MathContext(32, RoundingMode.HALF_UP);
        var numberText = new NumberText(BigDecimal.valueOf(5.3), mathContext);

        var json = toJson(numberText);
        System.out.println(json);
        assertNotNull(json);
    }

    public static String toJson(Object object) {
        try (var jsonb = JsonbBuilder.create(new JsonbConfig().withFormatting(true)
                .withNullValues(true)
                // using "PrivateVisibilityStrategy" leads to an exception
                .withPropertyVisibilityStrategy(new PrivateVisibilityStrategy()))) {
            return jsonb.toJson(object);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

}

I found the reason for this exception: I am using the PrivateVisibilityStrategy which leads to this exception. However, it's not very convenient to turn this off because in my case NumberText is just a property of another class hierarchy where I need the PrivateVisibilityStrategy. So if I disable it just because of MathContext, then the PrivateVisibilityStrategy for all other properties within the class hierarchy will also be disabled.

jbescos commented 2 years ago

I think the issue is that java.math module is not opened to java.base module, but you can explicitly open it with this JVM argument: --add-opens java.base/java.math=ALL-UNNAMED

Verdent commented 2 years ago

MathContext is not supported by Yasson currently. If you need this to be serialized/deserialized properly, please create your own custom Serializer/Deserializer for it.

nimo23 commented 1 year ago

@Verdent Look here with the current version:

Caused by: jakarta.json.bind.JsonbException: Unable to serialize property 'amount' from user.Item
    at deployment.test.war/org.eclipse.yasson.internal.serializer.ObjectSerializer.lambda$serialize$0(ObjectSerializer.java:43)
    at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.ObjectSerializer.serialize(ObjectSerializer.java:38)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.RecursionChecker.serialize(RecursionChecker.java:38)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.KeyWriter.serialize(KeyWriter.java:41)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.NullVisibilitySwitcher.serialize(NullVisibilitySwitcher.java:40)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.NullSerializer.serialize(NullSerializer.java:67)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.ObjectTypeSerializer.findSerializer(ObjectTypeSerializer.java:68)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.ObjectTypeSerializer.serializeValue(ObjectTypeSerializer.java:50)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.TypeSerializer$ValueSerializer.serialize(TypeSerializer.java:51)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.TypeSerializer.serialize(TypeSerializer.java:37)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.ObjectTypeSerializer.serialize(ObjectTypeSerializer.java:31)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.NullSerializer.serialize(NullSerializer.java:67)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.CollectionSerializer.lambda$serialize$0(CollectionSerializer.java:37)
    at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.CollectionSerializer.serialize(CollectionSerializer.java:37)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.KeyWriter.serialize(KeyWriter.java:41)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.NullVisibilitySwitcher.serialize(NullVisibilitySwitcher.java:40)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.NullSerializer.serialize(NullSerializer.java:67)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.ValueGetterSerializer.serialize(ValueGetterSerializer.java:43)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.ObjectSerializer.lambda$serialize$0(ObjectSerializer.java:41)
    ... 104 more
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field final java.math.RoundingMode java.math.MathContext.roundingMode accessible: module java.base does not "opens java.math" to unnamed module @765877f0
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
    at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
    at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:180)
    at java.base/java.lang.reflect.Field.setAccessible(Field.java:174)
    at deployment.test.war/org.eclipse.yasson.internal.model.PropertyModel.lambda$overrideAccessible$2(PropertyModel.java:594)
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:318)
    at deployment.test.war/org.eclipse.yasson.internal.model.PropertyModel.overrideAccessible(PropertyModel.java:593)
    at deployment.test.war/org.eclipse.yasson.internal.model.PropertyModel.isFieldVisible(PropertyModel.java:568)
    at deployment.test.war/org.eclipse.yasson.internal.model.PropertyModel.createReadHandle(PropertyModel.java:516)
    at deployment.test.war/org.eclipse.yasson.internal.model.PropertyModel.<init>(PropertyModel.java:157)
    at deployment.test.war/org.eclipse.yasson.internal.ClassParser.lambda$parseProperties$0(ClassParser.java:70)
    at java.base/java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:197)
    at java.base/java.util.HashMap$ValueSpliterator.forEachRemaining(HashMap.java:1779)
    at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:509)
    at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499)
    at java.base/java.util.stream.ReduceOps$ReduceOp.evaluateSequential(ReduceOps.java:921)
    at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.base/java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:682)
    at deployment.test.war/org.eclipse.yasson.internal.ClassParser.parseProperties(ClassParser.java:71)
    at deployment.test.war/org.eclipse.yasson.internal.MappingContext.lambda$createParseClassModelFunction$1(MappingContext.java:105)
    at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
    at deployment.test.war/org.eclipse.yasson.internal.MappingContext.getOrCreateClassModel(MappingContext.java:77)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.SerializationModelCreator.serializerChainInternal(SerializationModelCreator.java:187)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.SerializationModelCreator.serializerChain(SerializationModelCreator.java:137)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.SerializationModelCreator.memberSerializer(SerializationModelCreator.java:381)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.SerializationModelCreator.createObjectSerializer(SerializationModelCreator.java:213)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.SerializationModelCreator.serializerChainInternal(SerializationModelCreator.java:199)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.SerializationModelCreator.serializerChainRuntime(SerializationModelCreator.java:123)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.ObjectTypeSerializer.lambda$findSerializer$0(ObjectTypeSerializer.java:67)
    at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.ObjectTypeSerializer.findSerializer(ObjectTypeSerializer.java:65)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.ObjectTypeSerializer.serializeValue(ObjectTypeSerializer.java:50)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.TypeSerializer$ValueSerializer.serialize(TypeSerializer.java:51)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.TypeSerializer.serialize(TypeSerializer.java:37)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.types.ObjectTypeSerializer.serialize(ObjectTypeSerializer.java:31)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.NullSerializer.serialize(NullSerializer.java:67)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.ValueGetterSerializer.serialize(ValueGetterSerializer.java:43)
    at deployment.test.war/org.eclipse.yasson.internal.serializer.ObjectSerializer.lambda$serialize$0(ObjectSerializer.java:41)
    ... 124 more

MathContext is not supported by Yasson currently.

Why? It has only two properties final int precision; and final RoundingMode roundingMode; (or their getters getPrecision(), getRoundingMode()) which can be easily (de)serialized by yasson. I think I can remember that this was already possible in one of the earlier versions of Yasson, because I used that lib back then and didn't have these problems with exactly this class. But I don't remember which version was suddenly no longer possible. I think, in combination with (de)serializing BigDecimal, de)serializing MathContext as well has it needs..

What must be done to support it?

nimo23 commented 1 year ago

I can provide a PR. I wrote a MathContextAdapter:

public class MathContextAdapter implements JsonbAdapter<MathContext, JsonObject> {

    @Override
    public JsonObject adaptToJson(MathContext mathContext) throws Exception {
        return Json.createObjectBuilder()
                .add("precision", mathContext.getPrecision())
                .add("roundingMode", mathContext.getRoundingMode().name())
                .build();
    }

    @Override
    public MathContext adaptFromJson(JsonObject jsonObject) throws Exception {
        var precision = jsonObject.getInt("precision");
        var roundingMode = jsonObject.getString("roundingMode");
        MathContext mathContext = new MathContext(precision, RoundingMode.valueOf(roundingMode));
        return mathContext;
    }
}

However, looking into e.g. https://github.com/eclipse-ee4j/yasson/tree/master/src/main/java/org/eclipse/yasson/internal, Yasson only accepts serializers and deserializers. Should I then implement MathContextSerializer/MathContextDeserializer?

I think it would be good to support MathContext by default (so the user does not need to write a adapter) since it is a Java built-in class and like other classes (like ZoneId, which is also supported by Yasson) it is a common Java class.

nimo23 commented 1 year ago

Another question: Why do I even have to register such a (De)serializer to be able to (de)serialize this class? Normally Json-B can (de)serialize a common class like this (consisting only of a String and an Enum property) by default.