yasson icon indicating copy to clipboard operation
yasson copied to clipboard

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

Open nimo23 opened this issue 3 years ago • 7 comments

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:

  • OS: Mac
  • Java Version: 17
  • Yasson Version: 1.0.9

nimo23 avatar Aug 23 '22 14:08 nimo23

Could you provide a reproducer please?.

jbescos avatar Aug 25 '22 07:08 jbescos

@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.

nimo23 avatar Aug 25 '22 09:08 nimo23

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

jbescos avatar Aug 25 '22 10:08 jbescos

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.

Verdent avatar Aug 25 '22 11:08 Verdent

@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 avatar Sep 02 '23 22:09 nimo23

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 avatar Sep 04 '23 13:09 nimo23

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.

nimo23 avatar Sep 05 '23 09:09 nimo23