question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[RECALLED] Wrong value in state store when use custom json serializer

See original GitHub issue

I use dapr v1.0.0. I wrote the actor use Java SDK. To use the right OffsetDateTime class serialization I wrote a custom Json serializer class ISO8601DaprObjectSerializer extends ObjectSerializer implements DaprObjectSerializer.

Expected Behavior

When I use this serializer I want to see JSON in state store

Actual Behavior

Now in state store I see base64 encoded string. In this case, I should use a special deserialized object constructor with string parameter.

Steps to Reproduce the Problem

When register actor add custom serializer

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import io.dapr.client.ObjectSerializer;
import io.dapr.client.domain.CloudEvent;
import io.dapr.serializer.DaprObjectSerializer;
import io.dapr.utils.TypeRef;

import java.io.IOException;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.regex.Pattern;

public class ISO8601DaprObjectSerializer extends ObjectSerializer implements DaprObjectSerializer {

    private static final Pattern PATTERN_ISO8601 = Pattern.compile("^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])(\\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$");
    private static final DateTimeFormatter ISO8601 = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX").withZone(ZoneId.of("UTC"));

    private static final JavaTimeModule TIME_MODULE = new JavaTimeModule();
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper()
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
            .setSerializationInclusion(JsonInclude.Include.NON_NULL)
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
            .registerModule(TIME_MODULE);

    static  {
        TIME_MODULE.addSerializer(OffsetDateTime.class, new JsonSerializer<>() {
            @Override
            public void serialize(OffsetDateTime offsetDateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                jsonGenerator.writeString(offsetDateTime.format(ISO8601));
            }

        });
    }

    @Override
    public byte[] serialize(Object state) throws IOException {
        if (state == null) {
            return null;
        }

        if (state.getClass().equals(Void.class)) {
            return null;
        }

        // Have this check here to be consistent with deserialization (see deserialize() method below).
        if (state instanceof byte[]) {
            return (byte[]) state;
        }

        // Not string, not primitive, so it is a complex type: we use JSON for that.
        return OBJECT_MAPPER.writeValueAsBytes(state);
    }

    @Override
    public <T> T deserialize(byte[] content, TypeRef<T> type) throws IOException {
        var javaType = OBJECT_MAPPER.constructType(type.getType());

        if ((javaType == null) || javaType.isTypeOrSubTypeOf(Void.class)) {
            return null;
        }

        if (javaType.isPrimitive()) {
            return deserializePrimitives(content, javaType);
        }

        if (content == null) {
            return (T) null;
        }

        // Deserialization of GRPC response fails without this check since it does not come as base64 encoded byte[].
        if (javaType.hasRawClass(byte[].class)) {
            return (T) content;
        }

        if (content.length == 0) {
            return (T) null;
        }

        if (javaType.hasRawClass(CloudEvent.class)) {
            return (T) CloudEvent.deserialize(content);
        }

        return OBJECT_MAPPER.readValue(content, javaType);
    }

    @Override
    public String getContentType() {
        return "application/json";
    }

    /**
     * Parses a given String to the corresponding object defined by class.
     *
     * @param content  Value to be parsed.
     * @param javaType Type of the expected result type.
     * @param <T>      Result type.
     * @return Result as corresponding type.
     * @throws IOException if cannot deserialize primitive time.
     */
    private static <T> T deserializePrimitives(byte[] content, JavaType javaType) throws IOException {
        if ((content == null) || (content.length == 0)) {
            if (javaType.hasRawClass(boolean.class)) {
                return (T) Boolean.FALSE;
            }

            if (javaType.hasRawClass(byte.class)) {
                return (T) Byte.valueOf((byte) 0);
            }

            if (javaType.hasRawClass(short.class)) {
                return (T) Short.valueOf((short) 0);
            }

            if (javaType.hasRawClass(int.class)) {
                return (T) Integer.valueOf(0);
            }

            if (javaType.hasRawClass(long.class)) {
                return (T) Long.valueOf(0L);
            }

            if (javaType.hasRawClass(float.class)) {
                return (T) Float.valueOf(0);
            }

            if (javaType.hasRawClass(double.class)) {
                return (T) Double.valueOf(0);
            }

            if (javaType.hasRawClass(char.class)) {
                return (T) Character.valueOf(Character.MIN_VALUE);
            }

            return null;
        }

        return OBJECT_MAPPER.readValue(content, javaType);
    }

    public static ObjectMapper getMapper() {
        return OBJECT_MAPPER;
    }
}

Release Note

RELEASE NOTE: FIX Serialization bug in SDK where custom JSON serializer is handled as byte[]

Issue Analytics

  • State:open
  • Created 3 years ago
  • Comments:7 (7 by maintainers)

github_iconTop GitHub Comments

3reactions
abogdanov37commented, Mar 18, 2021

I understand the problem with breaking changes. I take some time to think about the way and add changes.

1reaction
abogdanov37commented, May 5, 2021

@abogdanov37 This fix must be undone because it is a breaking change. Data stored with the old version cannot be read with this patch. We are reverting this and undoing that fix in the patch release too.

@artursouza I add new PR with some new tests. Please describe with more detail which state can’t be deserialized. I spend many time but can’t reproduce that behavior.

Read more comments on GitHub >

github_iconTop Results From Across the Web

[RECALLED] Wrong value in state store when use custom ...
I use dapr v1.0.0. I wrote the actor use Java SDK. To use the right OffsetDateTime class serialization I wrote a custom Json...
Read more >
The Curious Incident of the State Store in Recovery in ksqlDB
Each time a query processes an event from the source table, it will serialize the event into the format required and upserts it...
Read more >
How to write custom converters for JSON serialization - .NET
Learn how to create custom converters for the JSON serialization classes that are provided in the System.Text.Json namespace.
Read more >
How to use custom serializers with KTable in Kafka streams?
So, my original KStream was just mapping the records from JSON to AVRO like this, and it is working fine. @StreamListener("notification-input- ...
Read more >
Writing custom classifiers - AWS Glue
You can create a custom classifier using a grok pattern, an XML tag, JavaScript Object Notation (JSON), or comma-separated values (CSV). An AWS...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found