micronaut-serialization icon indicating copy to clipboard operation
micronaut-serialization copied to clipboard

Json error is not deserialized correctly

Open melix opened this issue 1 year ago • 5 comments

Expected Behavior

It should be possible to deserialize to a JsonError class.

Actual Behaviour

Deserialization produces an incomplete result.

Steps To Reproduce

The following test case fails:

package io.micronaut.serde.jackson

import io.micronaut.http.hateoas.JsonError
import io.micronaut.http.hateoas.Resource
import io.micronaut.serde.ObjectMapper
import spock.lang.Specification

class JsonErrorSpec extends Specification {
    void "JsonError should be deserializable from a string"() {
        setup:
        ObjectMapper objectMapper = ObjectMapper.getDefault()

        when:
        JsonError deserializationResult = objectMapper.readValue('{"_links":{"self":[{"href":"/resolve","templated":false}]},"_embedded":{"errors":[{"message":"Internal Server Error: Something bad happened"}]},"message":"Internal Server Error"}', JsonError)

        then:
        deserializationResult.message == 'Internal Server Error'
        deserializationResult.embedded.getFirst('errors').present

    }

    void "can deserialize a Json error as a generic resource"() {
        setup:
        ObjectMapper objectMapper = ObjectMapper.getDefault()

        when:
        Resource deserializationResult = objectMapper.readValue('{"_links":{"self":[{"href":"/resolve","templated":false}]},"_embedded":{"errors":[{"message":"Internal Server Error: Something bad happened"}]},"message":"Internal Server Error"}', Resource)

        then:
        deserializationResult != null

    }
}

Environment Information

No response

Example Application

No response

Version

4.2.0

melix avatar Nov 22 '23 09:11 melix

We have the same issue. It would be great to get any update

andrebrov avatar Jan 11 '24 19:01 andrebrov

It's funny, but it never worked. I found a test written 2.5 years ago:

изображение

Here is a simplified class structure to reproduce the problem.

package io.micronaut.serde.support;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.annotation.ReflectiveAccess;
import io.micronaut.core.convert.value.ConvertibleValues;
import io.micronaut.core.util.StringUtils;
import io.micronaut.core.value.OptionalMultiValues;
import io.micronaut.http.hateoas.Link;
import io.micronaut.http.hateoas.Resource;
import io.micronaut.serde.annotation.Serdeable;

import com.fasterxml.jackson.annotation.JsonProperty;

import static io.micronaut.http.hateoas.Resource.EMBEDDED;
import static io.micronaut.http.hateoas.Resource.LINKS;

@Serdeable
public class TestJsonError<Impl extends TestJsonError> {

    private final Map<CharSequence, List<Link>> linkMap = new LinkedHashMap<>(1);
    private final Map<CharSequence, List<Resource>> embeddedMap = new LinkedHashMap<>(1);

    @JsonProperty(LINKS)
    public OptionalMultiValues<Link> getLinks() {
        return OptionalMultiValues.of(linkMap);
    }

    @JsonProperty(EMBEDDED)
    public OptionalMultiValues<Resource> getEmbedded() {
        return OptionalMultiValues.of(embeddedMap);
    }

    @SuppressWarnings("unchecked")
    @Internal
    @ReflectiveAccess
    @JsonProperty(LINKS)
    public final void setLinks(Map<String, Object> links) {
        for (Map.Entry<String, Object> entry : links.entrySet()) {
            String name = entry.getKey();
            Object value = entry.getValue();
            if (value instanceof Map) {
                Map<String, Object> linkMap = (Map<String, Object>) value;
                link(name, linkMap);
            }
        }
    }

    @Internal
    @ReflectiveAccess
    @JsonProperty(EMBEDDED)
    public final void setEmbedded(Map<String, List<Resource>> embedded) {
        embeddedMap.putAll(embedded);
    }

    private void link(String name, Map<String, Object> linkMap) {
        ConvertibleValues<Object> values = ConvertibleValues.of(linkMap);
        Optional<String> uri = values.get(Link.HREF, String.class);
        uri.ifPresent(uri1 -> {
            Link.Builder link = Link.build(uri1);
            values.get("templated", Boolean.class)
                .ifPresent(link::templated);
            values.get("hreflang", String.class)
                .ifPresent(link::hreflang);
            values.get("title", String.class)
                .ifPresent(link::title);
            values.get("profile", String.class)
                .ifPresent(link::profile);
            values.get("deprecation", String.class)
                .ifPresent(link::deprecation);
            link(name, link.build());
        });
    }

    public Impl link(@Nullable CharSequence ref, @Nullable Link link) {
        if (StringUtils.isNotEmpty(ref) && link != null) {
            List<Link> links = this.linkMap.computeIfAbsent(ref, charSequence -> new ArrayList<>());
            links.add(link);
        }
        return (Impl) this;
    }
}

The problem is clearly that serde cannot interpret the methods correctly :

    @JsonProperty(LINKS)
    public final void setLinks(Map<String, Object> links) {

and

    @JsonProperty(EMBEDDED)
    public final void setEmbedded(Map<String, List<Resource>> embedded) {

@dstepanov It turns out that the analyzer for JsonProperty is not working correctly now.

altro3 avatar Feb 04 '24 08:02 altro3

At this moment, we ignore setters with a different type than the getter. This needs to be adjusted in Core, possibly distinguishing read and write types.

dstepanov avatar Feb 05 '24 14:02 dstepanov

Hm.... is the problem in the core? Why can't we take the right method in the heart and work with it? I mean, get a method with a JsonProperty annotation, understand that this is a setter for a certain field, and perform the necessary transformations.

I'm thinking about how I would do it in openapi, maybe serde has a tighter binding to the core and it's not that easy to do it.

altro3 avatar Feb 05 '24 15:02 altro3

The introspection needs to be improved to allow read/write types; right now, it does PropertyElementQuery.of(ce).ignoreSettersWithDifferingType(true) if you use ce.getBeanProperties() the default value is to include different setters, so it might work.

dstepanov avatar Feb 05 '24 16:02 dstepanov