micronaut-serialization
micronaut-serialization copied to clipboard
Json error is not deserialized correctly
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
We have the same issue. It would be great to get any update
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.
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.
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.
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.