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

Can not deserialize json to enum value with Object-/Array-valued input, @JsonCreator #3280

Closed
peteryuanpan opened this issue Sep 17, 2021 · 3 comments
Milestone

Comments

@peteryuanpan
Copy link

peteryuanpan commented Sep 17, 2021

Describe the bug
can not deserialize json to enum value in a particular case

Version information
Which Jackson version(s) was this for?
2.11.0

To Reproduce
If you have a way to reproduce this with:

  1. Brief code sample/snippet: include here in preformatted/code section
package server.test202109;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fenbi.common.util.IgnorableObjectMapper;

import java.io.IOException;

/**
 * @author peteryuanpan
 */
public class TestJackSon {

    private static final String json1 = "{\"b\":\"x\"}";
    private static final String json2 = "{\"a\":\"1\", \"b\":\"x\"}";
    private static final String json3 = "{\"a\":[], \"b\":\"x\"}";
    private static final String json4 = "{\"a\":{}, \"b\":\"x\"}";
    private static final String json5 = "{\"b\":\"x\", \"a\":[]}";
    private static final String json6 = "{\"b\":\"y\", \"a\":{}}";

    private static final IgnorableObjectMapper objectMapper = new IgnorableObjectMapper();

    public static void main(String[] args) throws IOException {
        test(json1);
        test(json2);
        test(json3);
        test(json4);
        test(json5);
        test(json6);
    }

    private static void test(String json) throws IOException {
        Enum e = objectMapper.readValue(json, Enum.class);
        System.out.println(e == null);
    }

    enum Enum {
        x("x"),
        y("y"),
        z("z");
        private final String value;
        Enum(String value) {
            this.value = value;
        }
        @JsonCreator
        public static Enum getByValue(@JsonProperty("b") String value) {
            for (Enum e : Enum.values()) {
                if (e.value.equals(value)) {
                    return e;
                }
            }
            return null;
        }
    }
}
  1. Longer example stored somewhere else (diff repo, snippet), add a link
  2. Textual explanation: include here
    The output is
false
false
true
true
false
false

can not deserialize json3 and json4, because there is "]" or "}" before the parameter "b"

Expected behavior
all output is false

Additional context
Add any other context about the problem here.

@peteryuanpan peteryuanpan added the to-evaluate Issue that has been received but not yet evaluated label Sep 17, 2021
@peteryuanpan
Copy link
Author

I have read a little source code, please let me note down here

com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer

    public Object[] getParameters(SettableBeanProperty[] props)
        throws JsonMappingException
    {
        // quick check to see if anything else is needed
        if (_paramsNeeded > 0) {
            if (_paramsSeenBig == null) {
                int mask = _paramsSeen;
                // not optimal, could use `Integer.trailingZeroes()`, but for now should not
                // really matter for common cases
                for (int ix = 0, len = _creatorParameters.length; ix < len; ++ix, mask >>= 1) {
                    if ((mask & 1) == 0) {
                        _creatorParameters[ix] = _findMissing(props[ix]);
                    }
                }
            } else {
                final int len = _creatorParameters.length;
                for (int ix = 0; (ix = _paramsSeenBig.nextClearBit(ix)) < len; ++ix) {
                    _creatorParameters[ix] = _findMissing(props[ix]);
                }
            }
        }

        if (_context.isEnabled(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)) {
            for (int ix = 0; ix < props.length; ++ix) {
                if (_creatorParameters[ix] == null) {
                    SettableBeanProperty prop = props[ix];
                    _context.reportInputMismatch(prop,
                            "Null value for creator property '%s' (index %d); `DeserializationFeature.FAIL_ON_NULL_FOR_CREATOR_PARAMETERS` enabled",
                            prop.getName(), props[ix].getCreatorIndex());
                }
            }
        }
        return _creatorParameters;
    }

    public boolean assignParameter(SettableBeanProperty prop, Object value)
    {
        final int ix = prop.getCreatorIndex();
        _creatorParameters[ix] = value;
        if (_paramsSeenBig == null) {
            int old = _paramsSeen;
            int newValue = (old | (1 << ix));
            if (old != newValue) {
                _paramsSeen = newValue;
                if (--_paramsNeeded <= 0) {
                    // 29-Nov-2016, tatu: But! May still require Object Id value
                    return (_objectIdReader == null) || (_idValue != null);
                }
            }
        } else {
            if (!_paramsSeenBig.get(ix)) {
                _paramsSeenBig.set(ix);
                if (--_paramsNeeded <= 0) {
                    // 29-Nov-2016, tatu: But! May still require Object Id value
                }
            }
        }
        return false;
    }

com.fasterxml.jackson.databind.deser.std.FactoryBasedEnumDeserializer

    // Method to deserialize the Enum using property based methodology
    protected Object deserializeEnumUsingPropertyBased(final JsonParser p, final DeserializationContext ctxt,
    		final PropertyBasedCreator creator) throws IOException
    {
        PropertyValueBuffer buffer = creator.startBuilding(p, ctxt, null);
    
        JsonToken t = p.currentToken();
        for (; t == JsonToken.FIELD_NAME; t = p.nextToken()) {
            String propName = p.getCurrentName();
            p.nextToken(); // to point to value
    
            SettableBeanProperty creatorProp = creator.findCreatorProperty(propName);
            if (creatorProp != null) {
                buffer.assignParameter(creatorProp, _deserializeWithErrorWrapping(p, ctxt, creatorProp));
                continue;
            }
            if (buffer.readIdProperty(propName)) {
                continue;
            }
        }
        return creator.build(ctxt, buffer);
    }

com.fasterxml.jackson.core.json.ReaderBasedJsonParser

    @Override
    public final JsonToken nextToken() throws IOException
    {
        /* First: field names are special -- we will always tokenize
         * (part of) value along with field name to simplify
         * state handling. If so, can and need to use secondary token:
         */
        if (_currToken == JsonToken.FIELD_NAME) {
            return _nextAfterName();
        }
        // But if we didn't already have a name, and (partially?) decode number,
        // need to ensure no numeric information is leaked
        _numTypesValid = NR_UNKNOWN;
        if (_tokenIncomplete) {
            _skipString(); // only strings can be partial
        }
        int i = _skipWSOrEnd();
        if (i < 0) { // end-of-input
            // Should actually close/release things
            // like input source, symbol table and recyclable buffers now.
            close();
            return (_currToken = null);
        }
        // clear any data retained so far
        _binaryValue = null;

        // Closing scope?
        if (i == INT_RBRACKET || i == INT_RCURLY) {
            _closeScope(i);
            return _currToken;
        }

        // Nope: do we then expect a comma?
        if (_parsingContext.expectComma()) {
            i = _skipComma(i);

            // Was that a trailing comma?
            if ((_features & FEAT_MASK_TRAILING_COMMA) != 0) {
                if ((i == INT_RBRACKET) || (i == INT_RCURLY)) {
                    _closeScope(i);
                    return _currToken;
                }
            }
        }

        /* And should we now have a name? Always true for Object contexts, since
         * the intermediate 'expect-value' state is never retained.
         */
        boolean inObject = _parsingContext.inObject();
        if (inObject) {
            // First, field name itself:
            _updateNameLocation();
            String name = (i == INT_QUOTE) ? _parseName() : _handleOddName(i);
            _parsingContext.setCurrentName(name);
            _currToken = JsonToken.FIELD_NAME;
            i = _skipColon();
        }
        _updateLocation();

        // Ok: we must have a value... what is it?

        JsonToken t;

        switch (i) {
        case '"':
            _tokenIncomplete = true;
            t = JsonToken.VALUE_STRING;
            break;
        case '[':
            if (!inObject) {
                _parsingContext = _parsingContext.createChildArrayContext(_tokenInputRow, _tokenInputCol);
            }
            t = JsonToken.START_ARRAY;
            break;
        case '{':
            if (!inObject) {
                _parsingContext = _parsingContext.createChildObjectContext(_tokenInputRow, _tokenInputCol);
            }
            t = JsonToken.START_OBJECT;
            break;
        case '}':
            // Error: } is not valid at this point; valid closers have
            // been handled earlier
            _reportUnexpectedChar(i, "expected a value");
        case 't':
            _matchTrue();
            t = JsonToken.VALUE_TRUE;
            break;
        case 'f':
            _matchFalse();
            t = JsonToken.VALUE_FALSE;
            break;
        case 'n':
            _matchNull();
            t = JsonToken.VALUE_NULL;
            break;

        case '-':
            /* Should we have separate handling for plus? Although
             * it is not allowed per se, it may be erroneously used,
             * and could be indicate by a more specific error message.
             */
            t = _parseNegNumber();
            break;
        case '.': // [core#61]]
            t = _parseFloatThatStartsWithPeriod();
            break;
        case '0':
        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
        case '8':
        case '9':
            t = _parsePosNumber(i);
            break;
        default:
            t = _handleOddValue(i);
            break;
        }

        if (inObject) {
            _nextToken = t;
            return _currToken;
        }
        _currToken = t;
        return t;
    }

    private final int _skipWSOrEnd() throws IOException
    {
        // Let's handle first character separately since it is likely that
        // it is either non-whitespace; or we have longer run of white space
        if (_inputPtr >= _inputEnd) {
            if (!_loadMore()) {
                return _eofAsNextChar();
            }
        }
        int i = _inputBuffer[_inputPtr++];
        if (i > INT_SPACE) {
            if (i == INT_SLASH || i == INT_HASH) {
                --_inputPtr;
                return _skipWSOrEnd2();
            }
            return i;
        }
        if (i != INT_SPACE) {
            if (i == INT_LF) {
                ++_currInputRow;
                _currInputRowStart = _inputPtr;
            } else if (i == INT_CR) {
                _skipCR();
            } else if (i != INT_TAB) {
                _throwInvalidSpace(i);
            }
        }

        while (_inputPtr < _inputEnd) {
            i = (int) _inputBuffer[_inputPtr++];
            if (i > INT_SPACE) {
                if (i == INT_SLASH || i == INT_HASH) {
                    --_inputPtr;
                    return _skipWSOrEnd2();
                }
                return i;
            }
            if (i != INT_SPACE) {
                if (i == INT_LF) {
                    ++_currInputRow;
                    _currInputRowStart = _inputPtr;
                } else if (i == INT_CR) {
                    _skipCR();
                } else if (i != INT_TAB) {
                    _throwInvalidSpace(i);
                }
            }
        }
        return _skipWSOrEnd2();
    }

    private void _closeScope(int i) throws JsonParseException {
        if (i == INT_RBRACKET) {
            _updateLocation();
            if (!_parsingContext.inArray()) {
                _reportMismatchedEndMarker(i, '}');
            }
            _parsingContext = _parsingContext.clearAndGetParent();
            _currToken = JsonToken.END_ARRAY;
        }
        if (i == INT_RCURLY) {
            _updateLocation();
            if (!_parsingContext.inObject()) {
                _reportMismatchedEndMarker(i, ']');
            }
            _parsingContext = _parsingContext.clearAndGetParent();
            _currToken = JsonToken.END_OBJECT;
        }
    }

@cowtowncoder cowtowncoder added 2.13 and removed to-evaluate Issue that has been received but not yet evaluated labels Sep 28, 2021
@cowtowncoder
Copy link
Member

Hi @peteryuanpan! Thank you for reporting this, I will have a look.

@cowtowncoder cowtowncoder changed the title can not deserialize json to enum value in a particular case Can not deserialize json to enum value with Object-/Array-valued input, @JsonCreator Sep 28, 2021
@cowtowncoder cowtowncoder added this to the 2.13.0 milestone Sep 28, 2021
@cowtowncoder
Copy link
Member

Ok, looks like what was missing is the skipping of content for structured values (Object, Array) -- they consist of more than one token. So needed to add:

p.skipChildren();

within deserializeEnumUsingPropertyBased() of FactoryBasedEnumDeserializer.

Fix will be in 2.13.0; I merged it in 2.12 but not sure if there will be new patch releases from there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants