Skip to content
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

Fix Enum deserialization with JsonFormat.Shape.OBJECT using both DELEGATING and PROPERTIES creator modes #3851

Merged
merged 6 commits into from
Apr 1, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.fasterxml.jackson.databind.deser.std;

import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;

import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
Expand Down Expand Up @@ -40,10 +41,12 @@ class FactoryBasedEnumDeserializer

/**
* Lazily instantiated property-based creator.
* Introduced in 2.8 and wrapped with {@link AtomicReference} in 2.15
*
* @since 2.15
*
* @since 2.8
*/
private transient PropertyBasedCreator _propCreator;
private AtomicReference<PropertyBasedCreator> _propCreatorRef = new AtomicReference<>(null);

public FactoryBasedEnumDeserializer(Class<?> cls, AnnotatedMethod f, JavaType paramType,
ValueInstantiator valueInstantiator, SettableBeanProperty[] creatorProps)
Expand Down Expand Up @@ -132,18 +135,21 @@ public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOEx
// 30-Mar-2020, tatu: For properties-based one, MUST get JSON Object (before
// 2.11, was just assuming match)
if (_creatorProps != null) {
if (!p.isExpectedStartObjectToken()) {
if (p.isExpectedStartObjectToken()) {
_propCreatorRef.compareAndSet(null,
Copy link
Member

Choose a reason for hiding this comment

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

But wouldn't this create (and try set) a new instance every time? :-D
(instead of trying to get() and only create-and-set if null)

Copy link
Member Author

@JooHyukKim JooHyukKim Mar 31, 2023

Choose a reason for hiding this comment

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

this create (and try set) a new instance every time? :-D
hmm, Not really my intention 😅

Could you check again? Take below as an example. Invoking get() and set separately wouldn't be atomic anymore 🤔.

    public void testAtomicRef() {
        Bean expected = new Bean(0);

        AtomicReference<Bean> reference = new AtomicReference<>(null);
        reference.compareAndSet(null, expected);
        reference.compareAndSet(null, new Bean(1));
        reference.compareAndSet(null, new Bean(2));

       // Passes
        assertEquals(0,reference.get().a);
    }

According to compareAndSet documentation,

Atomically sets the value to newValue if the current value == expectedValue, with memory effects

Copy link
Member Author

Choose a reason for hiding this comment

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

Or we can do good-ol' double-checked locking like ones done in EnumDeserializer below.

protected CompactStringObjectMap _getToStringLookup(DeserializationContext ctxt) {
CompactStringObjectMap lookup = _lookupByToString;
if (lookup == null) {
synchronized (this) {
lookup = _lookupByToString;
if (lookup == null) {
lookup = EnumResolver.constructUsingToString(ctxt.getConfig(), _enumClass())
.constructLookup();
_lookupByToString = lookup;
}
}
}
return lookup;
}

Copy link
Member

Choose a reason for hiding this comment

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

No, my problem is that instead of only creating it if needed, it creates new instance every time and sets -- and never uses value. You are correct in that get + set is not atomic, but I am not worried about atomicity here; if instance gets created 2 or 3 times that's fine. They are all identical. Goal is just to avoid always creating instance eagerly.

Copy link
Member

Choose a reason for hiding this comment

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

And no need to do double-locking with synchronized: just do get() from reference; if (and only if) null returned, construct instance, set() (or compareAndSet() against null). Use the props gotten or created.

Or leave as-is and I can add that as well, not a big deal.

Copy link
Member Author

@JooHyukKim JooHyukKim Mar 31, 2023

Choose a reason for hiding this comment

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

No, my problem is that instead of only creating it if needed, it creates new instance every time and sets -- and never uses value.

Oh, I think I understand now! Thank you for the explanation! 🙏🏼🙏🏼.

The code creates newly instance in compareAndSet(null, PropertyBasedCreator.construct()). The new intance will be ignored, but created "everytime" when _propCreator is not null anymore.

PropertyBasedCreator.construct(ctxt, _valueInstantiator, _creatorProps,
ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES)));

p.nextToken();
return deserializeEnumUsingPropertyBased(p, ctxt, _propCreatorRef.get());
}
// If value cannot possibly be delegating-creator,
if (!_valueInstantiator.canCreateFromString()) {
final JavaType targetType = getValueType(ctxt);
ctxt.reportInputMismatch(targetType,
"Input mismatch reading Enum %s: properties-based `@JsonCreator` (%s) expects JSON Object (JsonToken.START_OBJECT), got JsonToken.%s",
ClassUtil.getTypeDescription(targetType), _factory, p.currentToken());
}
if (_propCreator == null) {
_propCreator = PropertyBasedCreator.construct(ctxt, _valueInstantiator, _creatorProps,
ctxt.isEnabled(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES));
"Input mismatch reading Enum %s: properties-based `@JsonCreator` (%s) expects JSON Object (JsonToken.START_OBJECT), got JsonToken.%s",
ClassUtil.getTypeDescription(targetType), _factory, p.currentToken());
}
p.nextToken();
return deserializeEnumUsingPropertyBased(p, ctxt, _propCreator);
}
Copy link
Member Author

@JooHyukKim JooHyukKim Mar 29, 2023

Choose a reason for hiding this comment

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

Summary here,

  1. Simply flipped the p.isExpectedStartObjectToken() conditional (line 135)
  2. Allow pass-through of case for possible "delegating-creator" (line 144) -- in other words, "fail if f value cannot possibly be delegating-creator"


// 12-Oct-2021, tatu: We really should only get here if and when String
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.fasterxml.jackson.failing;
package com.fasterxml.jackson.databind.deser.creators;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonFormat;
Expand Down Expand Up @@ -98,7 +98,8 @@ public static EnumB fromString(String type) {
@JsonFormat(shape = JsonFormat.Shape.OBJECT)
enum EnumC {
A("AType"),
B("BType");
B("BType"),
C("CType");

private final String type;

Expand All @@ -121,6 +122,16 @@ public static EnumC create(@JsonProperty("type") String type) {
}
}

static class DelegatingCreatorEnumWrapper {
public EnumA enumA;
public EnumB enumB;
}

static class PropertiesCreatorEnumWrapper {
public EnumA enumA;
public EnumC enumC;
}

/*
/**********************************************************
/* Tests
Expand Down Expand Up @@ -179,4 +190,21 @@ public void testPojoCreatorModeDelegating() throws Exception {

assertEquals("properties", pojo1.name);
}

public void testDelegatingCreatorEnumWrapper() throws Exception {
DelegatingCreatorEnumWrapper wrapper = newJsonMapper()
.readValue(a2q("{'enumA':'AType', 'enumB': 'BType'}"), DelegatingCreatorEnumWrapper.class);

assertEquals(EnumA.A, wrapper.enumA);
assertEquals(EnumB.B, wrapper.enumB);
}

public void testPropertiesCreatorEnumWrapper() throws Exception {
PropertiesCreatorEnumWrapper wrapper = newJsonMapper()
.readValue(a2q("{'enumA':{'type':'AType'}, 'enumC': {'type':'CType'}}"), PropertiesCreatorEnumWrapper.class);

assertEquals(EnumA.A, wrapper.enumA);
assertEquals(EnumC.C, wrapper.enumC);
}

}