Repository: EventStore/EventStoreDB-Client-Java (java)

Issue:
Reading metadata for soft deleted stream results in "ClassCastException"
Reading the stream metadata for a soft deleted stream fails with a "ClassCastException".

It seems that field "StreamMetadata.truncateBefore" should be "Long" and not "Integer".

```
Caused by: java.lang.ClassCastException: class java.lang.Long cannot be cast to class java.lang.Integer (java.lang.Long and java.lang.Integer are in module java.base of loader 'bootstrap')
	at com.eventstore.dbclient.StreamMetadata.deserialize(StreamMetadata.java:148)
	at com.eventstore.dbclient.EventStoreDBClient.lambda$getStreamMetadata$1(EventStoreDBClient.java:187)
	at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1072)
```
Example program:

```java
    public static void main(String[] args) throws InterruptedException, ExecutionException {

        EventStoreDBClientSettings settings = EventStoreDBConnectionString.parseOrThrow("esdb://localhost:2113?tls=false");
        EventStoreDBClient client = EventStoreDBClient.create(settings);            	    	

        String streamId = "MyStream";
        
        // Create stream by appending something to it
        List<EventData> events = new ArrayList<>();
        events.add(EventData.builderAsBinary(UUID.randomUUID(), "baz", "dummy content".getBytes()).build());
        client.appendToStream(streamId, AppendToStreamOptions.get().expectedRevision(ExpectedRevision.any()), events.iterator()).join();

        // Delete
        client.deleteStream(streamId, DeleteStreamOptions.get().expectedRevision(ExpectedRevision.any())).join();
        
        // Get status
        client.getStreamMetadata(streamId).get();

    }
```


Pull request description:
Improve stream metadata serialization.
Fixed: Improve stream metadata serialization.

Fixes #240 

Patch:
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/Acl.java b/db-client-java/src/main/java/com/eventstore/dbclient/Acl.java
index e7eb50a..ef849da 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/Acl.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/Acl.java
@@ -3,6 +3,4 @@ package com.eventstore.dbclient;
 /**
  * Common access control list (ACL) interface.
  */
-public interface Acl {
-    Object serialize();
-}
+public interface Acl {}
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/Acls.java b/db-client-java/src/main/java/com/eventstore/dbclient/Acls.java
index 647c677..c0ac1a7 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/Acls.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/Acls.java
@@ -1,7 +1,5 @@
 package com.eventstore.dbclient;
 
-import java.util.HashMap;
-
 /**
  * Access control list (ACL) utility class.
  */
@@ -33,23 +31,4 @@ public final class Acls {
     public static Acl newSystemStreamAcl() {
         return SystemStreamAcl.getInstance();
     }
-
-    @SuppressWarnings("unchecked")
-    static Acl deserialize(Object source) {
-        Acl acl = null;
-
-        if (source != null) {
-            if (source instanceof HashMap) {
-                acl = StreamAcl.deserialize((HashMap<String, Object>) source);
-            } else if (source instanceof String) {
-                String str = (String) source;
-                acl = UserStreamAcl.deserialize(str);
-                acl = acl == null ? SystemStreamAcl.deserialize(str) : acl;
-            } else {
-                throw new RuntimeException("Unsupported type for ACL: " + source.getClass());
-            }
-        }
-
-        return acl;
-    }
 }
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/CustomAclCodec.java b/db-client-java/src/main/java/com/eventstore/dbclient/CustomAclCodec.java
new file mode 100644
index 0000000..37e55c7
--- /dev/null
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/CustomAclCodec.java
@@ -0,0 +1,87 @@
+package com.eventstore.dbclient;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public final class CustomAclCodec {
+    public static class ListSerializer extends JsonSerializer<List<String>> {
+        @Override
+        public void serialize(List<String> value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+            if (value.size() == 1) {
+                gen.writeString(value.get(0));
+            } else {
+                gen.writeStartArray();
+
+                for (String s: value) {
+                    gen.writeString(s);
+                }
+
+                gen.writeEndArray();
+            }
+        }
+    }
+
+    public static class ListDeserializer extends JsonDeserializer<List<String>> {
+        @Override
+        public List<String> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+            if (p.isExpectedStartArrayToken()) {
+                List<String> list = new ArrayList<>();
+
+                while (!p.nextToken().isStructEnd()) {
+                    list.add(p.getValueAsString());
+                }
+
+                return list;
+            }
+
+            return Collections.singletonList(p.getValueAsString());
+        }
+    }
+
+    public static class Serializer extends JsonSerializer<Acl> {
+        @Override
+        public void serialize(Acl value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
+            if (value instanceof SystemStreamAcl) {
+                gen.writeString(SystemStreamAcl.ACL_NAME);
+                return;
+            }
+
+            if (value instanceof UserStreamAcl) {
+                gen.writeString(UserStreamAcl.ACL_NAME);
+                return;
+            }
+
+            gen.writePOJO(value);
+        }
+    }
+
+    public static class Deserializer extends JsonDeserializer<Acl> {
+        @Override
+        public Acl deserialize(JsonParser p, DeserializationContext ctx) throws IOException {
+            if (p.currentToken() == JsonToken.VALUE_STRING) {
+                String value = p.getText();
+
+                if (value.equals(UserStreamAcl.ACL_NAME))
+                    return UserStreamAcl.getInstance();
+
+                if (value.equals(SystemStreamAcl.ACL_NAME)) {
+                    return SystemStreamAcl.getInstance();
+                }
+
+                throw new IOException(String.format("Unknown ACL type '%s'", value));
+            }
+
+            return p.readValueAs(StreamAcl.class);
+        }
+    }
+}
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClient.java b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClient.java
index 6c243e8..137afd6 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClient.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/EventStoreDBClient.java
@@ -1,16 +1,14 @@
 package com.eventstore.dbclient;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.json.JsonMapper;
 import org.reactivestreams.Publisher;
-import org.reactivestreams.Subscriber;
 
 import java.util.Arrays;
-import java.util.HashMap;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.CompletableFuture;
-import java.util.function.Function;
 
 /**
  * Represents EventStoreDB client for stream operations. A client instance maintains a two-way communication to EventStoreDB.
@@ -97,9 +95,14 @@ public class EventStoreDBClient extends EventStoreDBClientBase {
      * @return a write result if successful.
      */
     public CompletableFuture<WriteResult> setStreamMetadata(String streamName, AppendToStreamOptions options, StreamMetadata metadata) {
-        EventData event = EventDataBuilder.json("$metadata", metadata.serialize()).build();
+        JsonMapper mapper = new JsonMapper();
 
-        return appendToStream("$$" + streamName, options, event);
+        try {
+            EventData event = EventDataBuilder.json("$metadata", mapper.writeValueAsBytes(metadata)).build();
+            return appendToStream("$$" + streamName, options, event);
+        } catch (JsonProcessingException e) {
+            throw new RuntimeException(e);
+        }
      }
 
     /**
@@ -132,32 +135,6 @@ public class EventStoreDBClient extends EventStoreDBClientBase {
         return new ReadStream(this.getGrpcClient(), streamName, options);
     }
 
-    private static <A, B> Publisher<B> publisherMap(final Publisher<A> parent, Function<A, B> fun) {
-        return sub -> {
-            parent.subscribe(new Subscriber<A>() {
-                @Override
-                public void onSubscribe(org.reactivestreams.Subscription s) {
-                    sub.onSubscribe(s);
-                }
-
-                @Override
-                public void onNext(A a) {
-                    sub.onNext(fun.apply(a));
-                }
-
-                @Override
-                public void onError(Throwable t) {
-                    sub.onError(t);
-                }
-
-                @Override
-                public void onComplete() {
-                    sub.onComplete();
-                }
-            });
-        };
-    }
-
     /**
      * Reads stream's metadata.
      * @param streamName stream's name.
@@ -181,10 +158,8 @@ public class EventStoreDBClient extends EventStoreDBClientBase {
 
             try {
                 JsonMapper mapper = new JsonMapper();
-                @SuppressWarnings("unchecked")
-                HashMap<String, Object> source = mapper.readValue(event.getEventData(), HashMap.class);
-
-                out.complete(StreamMetadata.deserialize(source));
+                StreamMetadata metadata = mapper.readValue(event.getEventData(), StreamMetadata.class);
+                out.complete(metadata);
             } catch (Throwable e) {
                 out.completeExceptionally(e);
             }
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/StreamAcl.java b/db-client-java/src/main/java/com/eventstore/dbclient/StreamAcl.java
index 13d9886..3c83f61 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/StreamAcl.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/StreamAcl.java
@@ -1,16 +1,41 @@
 package com.eventstore.dbclient;
 
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
 import java.util.*;
 
 /**
  * Stream-related access control list (ACL).
  */
+@JsonInclude(JsonInclude.Include.NON_NULL)
 public class StreamAcl implements Acl {
-    private ArrayList<String> readRoles;
-    private ArrayList<String> writeRoles;
-    private ArrayList<String> deleteRoles;
-    private ArrayList<String> metaReadRoles;
-    private ArrayList<String> metaWriteRoles;
+    @JsonProperty("$r")
+    @JsonSerialize(using = CustomAclCodec.ListSerializer.class)
+    @JsonDeserialize(using = CustomAclCodec.ListDeserializer.class)
+    private List<String> readRoles;
+
+    @JsonProperty("$w")
+    @JsonSerialize(using = CustomAclCodec.ListSerializer.class)
+    @JsonDeserialize(using = CustomAclCodec.ListDeserializer.class)
+    private List<String> writeRoles;
+
+    @JsonProperty("$d")
+    @JsonSerialize(using = CustomAclCodec.ListSerializer.class)
+    @JsonDeserialize(using = CustomAclCodec.ListDeserializer.class)
+    private List<String> deleteRoles;
+
+    @JsonProperty("$mr")
+    @JsonSerialize(using = CustomAclCodec.ListSerializer.class)
+    @JsonDeserialize(using = CustomAclCodec.ListDeserializer.class)
+    private List<String> metaReadRoles;
+
+    @JsonProperty("$mw")
+    @JsonSerialize(using = CustomAclCodec.ListSerializer.class)
+    @JsonDeserialize(using = CustomAclCodec.ListDeserializer.class)
+    private List<String> metaWriteRoles;
 
     /**
      * Adds read roles.
@@ -67,93 +92,38 @@ public class StreamAcl implements Acl {
         return this;
     }
 
-    private static void serializeRoles(HashMap<String, Object> output, String key, ArrayList<String> target) {
-        if (target == null)
-            return;
-
-        if (target.size() == 1) {
-            output.put(key, target.get(0));
-        } else {
-            output.put(key, target);
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    private static ArrayList<String> deserializeRoles(HashMap<String, Object> source, String key) {
-        ArrayList<String> list = null;
-        Object value = source.get(key);
-
-        if (value != null) {
-            list = new ArrayList<>();
-
-            if (value instanceof String) {
-                list.add((String) value);
-            } else if (value instanceof ArrayList) {
-                list.addAll((ArrayList<String>) value);
-            } else {
-                throw new RuntimeException("Unsupported role type: " + value.getClass());
-            }
-        }
-
-        return list;
-    }
-
-    @Override
-    public Object serialize() {
-        HashMap<String, Object> output = new HashMap<>();
-        serializeRoles(output, "$r", this.readRoles);
-        serializeRoles(output, "$w", this.writeRoles);
-        serializeRoles(output, "$d", this.deleteRoles);
-        serializeRoles(output, "$mr", this.metaReadRoles);
-        serializeRoles(output, "$mw", this.metaWriteRoles);
-
-        return output;
-    }
-
-     static StreamAcl deserialize(HashMap<String, Object> source) {
-        StreamAcl acl = new StreamAcl();
-
-        acl.readRoles = deserializeRoles(source, "$r");
-        acl.writeRoles = deserializeRoles(source, "$w");
-        acl.deleteRoles = deserializeRoles(source, "$d");
-        acl.metaReadRoles = deserializeRoles(source, "$mr");
-        acl.metaWriteRoles = deserializeRoles(source, "$mw");
-
-        return acl;
-    }
-
     /**
      * Returns read roles.
      */
-    public ArrayList<String> getReadRoles() {
+    public List<String> getReadRoles() {
         return readRoles;
     }
 
     /**
      * Returns write roles.
      */
-    public ArrayList<String> getWriteRoles() {
+    public List<String> getWriteRoles() {
         return writeRoles;
     }
 
     /**
      * Returns delete roles.
      */
-    public ArrayList<String> getDeleteRoles() {
+    public List<String> getDeleteRoles() {
         return deleteRoles;
     }
 
     /**
      * Return metadata read roles.
      */
-    public ArrayList<String> getMetaReadRoles() {
+    public List<String> getMetaReadRoles() {
         return metaReadRoles;
     }
 
     /**
      * Return metadata write roles.
      */
-    public ArrayList<String> getMetaWriteRoles() {
+    public List<String> getMetaWriteRoles() {
         return metaWriteRoles;
     }
 
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/StreamMetadata.java b/db-client-java/src/main/java/com/eventstore/dbclient/StreamMetadata.java
index a1a4ca1..cc11858 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/StreamMetadata.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/StreamMetadata.java
@@ -1,6 +1,9 @@
 package com.eventstore.dbclient;
 
-import java.util.HashMap;
+import com.fasterxml.jackson.annotation.*;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+
 import java.util.Map;
 import java.util.Objects;
 
@@ -8,25 +11,39 @@ import java.util.Objects;
  * Represents stream metadata with strongly typed properties for system values and a dictionary-like interface for
  * custom values.
  */
+@JsonInclude(JsonInclude.Include.NON_NULL)
 public class StreamMetadata {
-    private Integer maxAge;
-    private Integer truncateBefore;
-    private Integer cacheControl;
+    @JsonProperty("$maxAge")
+    private Long maxAge;
+
+    @JsonProperty("$tb")
+    private Long truncateBefore;
+
+    @JsonProperty("$cacheControl")
+    private Long cacheControl;
+
+    @JsonProperty("$acl")
+    @JsonSerialize(using = CustomAclCodec.Serializer.class)
+    @JsonDeserialize(using = CustomAclCodec.Deserializer.class)
     private Acl acl;
-    private Integer maxCount;
-    private HashMap<String, Object> customProperties;
+
+    @JsonProperty("$maxCount")
+    private Long maxCount;
+
+    @JsonAnySetter
+    private Map<String, Object> customProperties;
 
     /**
      * The maximum age of events allowed in the stream.
      */
-    public Integer getMaxAge() {
+    public Long getMaxAge() {
         return maxAge;
     }
 
     /**
      * The maximum age of events allowed in the stream.
      */
-    public void setMaxAge(Integer maxAge) {
+    public void setMaxAge(Long maxAge) {
         this.maxAge = maxAge;
     }
 
@@ -34,7 +51,7 @@ public class StreamMetadata {
      * The event number from which previous events can be scavenged. This is used to implement deletion of
      * streams.
      */
-    public Integer getTruncateBefore() {
+    public Long getTruncateBefore() {
         return truncateBefore;
     }
 
@@ -42,21 +59,21 @@ public class StreamMetadata {
      * The event number from which previous events can be scavenged. This is used to implement deletion of
      * streams.
      */
-    public void setTruncateBefore(Integer truncateBefore) {
+    public void setTruncateBefore(Long truncateBefore) {
         this.truncateBefore = truncateBefore;
     }
 
     /**
      * The amount of time for which the stream head is cacheable (in seconds).
      */
-    public Integer getCacheControl() {
+    public Long getCacheControl() {
         return cacheControl;
     }
 
     /**
      * The amount of time for which the stream head is cacheable (in seconds).
      */
-    public void setCacheControl(Integer cacheControl) {
+    public void setCacheControl(Long cacheControl) {
         this.cacheControl = cacheControl;
     }
 
@@ -77,94 +94,32 @@ public class StreamMetadata {
     /**
      * The maximum number of events allowed in the stream.
      */
-    public Integer getMaxCount() {
+    public Long getMaxCount() {
         return maxCount;
     }
 
     /**
      * The maximum number of events allowed in the stream.
      */
-    public void setMaxCount(Integer maxCount) {
+    public void setMaxCount(Long maxCount) {
         this.maxCount = maxCount;
     }
 
     /**
      * An enumerable of key-value pairs of keys to JSON text for user-provider metadata.
      */
-    public HashMap<String, Object> getCustomProperties() {
+    @JsonAnyGetter
+    public Map<String, Object> getCustomProperties() {
         return customProperties;
     }
 
     /**
      * An enumerable of key-value pairs of keys to JSON text for user-provider metadata.
      */
-    public void setCustomProperties(HashMap<String, Object> customProperties) {
+    public void setCustomProperties(Map<String, Object> customProperties) {
         this.customProperties = customProperties;
     }
 
-    static private void insertValue(HashMap<String, Object> output, String key, Object value) {
-        if (value != null) {
-            output.put(key, value);
-        }
-    }
-
-    public Object serialize() {
-        HashMap<String, Object> output = new HashMap<>();
-
-        insertValue(output, "$maxAge", this.maxAge);
-        insertValue(output, "$maxCount", this.maxCount);
-        insertValue(output, "$tb", this.truncateBefore);
-        insertValue(output, "$cacheControl", this.cacheControl);
-
-        if (this.acl != null) {
-            insertValue(output, "$acl", this.acl.serialize());
-        }
-
-        if (this.customProperties != null) {
-            this.customProperties.forEach((key, value) -> {
-                if (key.startsWith("$"))
-                    return;
-
-                insertValue(output, key, value);
-            });
-        }
-
-        return output;
-    }
-
-     public static StreamMetadata deserialize(HashMap<String, Object> source) {
-        StreamMetadata metadata = new StreamMetadata();
-        HashMap<String, Object> customProperties = null;
-
-        for (Map.Entry<String, Object> entry : source.entrySet()) {
-            switch (entry.getKey()) {
-                case "$maxAge":
-                    metadata.setMaxAge((Integer) entry.getValue());
-                    break;
-                case "$maxCount":
-                    metadata.setMaxCount((Integer) entry.getValue());
-                    break;
-                case "$tb":
-                    metadata.setTruncateBefore((Integer) entry.getValue());
-                    break;
-                case "$cacheControl":
-                    metadata.setCacheControl((Integer) entry.getValue());
-                    break;
-                case "$acl":
-                    metadata.setAcl(Acls.deserialize(entry.getValue()));
-                    break;
-                default:
-                    customProperties = customProperties == null ? new HashMap<>() : customProperties;
-                    customProperties.put(entry.getKey(), entry.getValue());
-                    break;
-            }
-        }
-
-        metadata.setCustomProperties(customProperties);
-
-        return metadata;
-    }
-
     @Override
     public boolean equals(Object o) {
         if (this == o) return true;
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/SystemStreamAcl.java b/db-client-java/src/main/java/com/eventstore/dbclient/SystemStreamAcl.java
index ba1d63e..4e2adb5 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/SystemStreamAcl.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/SystemStreamAcl.java
@@ -4,7 +4,7 @@ package com.eventstore.dbclient;
  * Admin stream access control list (ACL).
  */
 class SystemStreamAcl implements Acl {
-    private static final String ACL_NAME = "$systemStreamAcl";
+    public static final String ACL_NAME = "$systemStreamAcl";
     private static final SystemStreamAcl SINGLETON = new SystemStreamAcl();
 
     private SystemStreamAcl() {}
@@ -22,11 +22,6 @@ class SystemStreamAcl implements Acl {
         return SINGLETON;
     }
 
-    @Override
-    public Object serialize() {
-        return ACL_NAME;
-    }
-
     @Override
     public int hashCode() {
         return ACL_NAME.hashCode();
diff --git a/db-client-java/src/main/java/com/eventstore/dbclient/UserStreamAcl.java b/db-client-java/src/main/java/com/eventstore/dbclient/UserStreamAcl.java
index b54a5a8..f0180d7 100644
--- a/db-client-java/src/main/java/com/eventstore/dbclient/UserStreamAcl.java
+++ b/db-client-java/src/main/java/com/eventstore/dbclient/UserStreamAcl.java
@@ -4,7 +4,7 @@ package com.eventstore.dbclient;
  * Default user stream access control list (ACL).
  */
 class UserStreamAcl implements Acl {
-    private static final String ACL_NAME = "$userStreamAcl";
+    public static final String ACL_NAME = "$userStreamAcl";
     private static final UserStreamAcl SINGLETON = new UserStreamAcl();
 
     private UserStreamAcl() {}
@@ -22,11 +22,6 @@ class UserStreamAcl implements Acl {
         return SINGLETON;
     }
 
-    @Override
-    public Object serialize() {
-        return ACL_NAME;
-    }
-
     @Override
     public int hashCode() {
         return ACL_NAME.hashCode();


Test patch:
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/misc/OfflineMetadataTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/misc/OfflineMetadataTests.java
index c6ff7b9..f71ecf9 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/misc/OfflineMetadataTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/misc/OfflineMetadataTests.java
@@ -9,7 +9,6 @@ import org.junit.jupiter.api.Test;
 
 import java.util.HashMap;
 
-@SuppressWarnings("unchecked")
 public class OfflineMetadataTests {
     @Test
     public void testSerializationIsoMorphism() throws Throwable {
@@ -18,10 +17,10 @@ public class OfflineMetadataTests {
 
         custom.put("foo", "bar");
 
-        expected.setMaxAge(2);
-        expected.setCacheControl(15);
-        expected.setTruncateBefore(1);
-        expected.setMaxCount(12);
+        expected.setMaxAge(2L);
+        expected.setCacheControl(15L);
+        expected.setTruncateBefore(1L);
+        expected.setMaxCount(12L);
 
         Acl acl = Acls.newStreamAcl()
                 .addReadRoles("admin")
@@ -34,9 +33,7 @@ public class OfflineMetadataTests {
         expected.setCustomProperties(custom);
 
         ObjectMapper mapper = new ObjectMapper();
-        HashMap<String, Object> source = mapper.readValue(mapper.writeValueAsString(expected.serialize()), HashMap.class);
-        StreamMetadata actual = StreamMetadata.deserialize(source);
-
+        StreamMetadata actual = mapper.readValue(mapper.writeValueAsString(expected), StreamMetadata.class);
 
         Assertions.assertEquals(expected, actual);
     }
@@ -45,15 +42,13 @@ public class OfflineMetadataTests {
     public void testNullAcl() throws Throwable {
         StreamMetadata expected = new StreamMetadata();
 
-        expected.setMaxAge(2);
-        expected.setCacheControl(15);
-        expected.setTruncateBefore(1);
-        expected.setMaxCount(12);
+        expected.setMaxAge(2L);
+        expected.setCacheControl(15L);
+        expected.setTruncateBefore(1L);
+        expected.setMaxCount(12L);
 
         ObjectMapper mapper = new ObjectMapper();
-        HashMap<String, Object> source = mapper.readValue(mapper.writeValueAsString(expected.serialize()), HashMap.class);
-        StreamMetadata actual = StreamMetadata.deserialize(source);
-
+        StreamMetadata actual = mapper.readValue(mapper.writeValueAsString(expected), StreamMetadata.class);
 
         Assertions.assertEquals(expected, actual);
     }
diff --git a/db-client-java/src/test/java/com/eventstore/dbclient/streams/MetadataTests.java b/db-client-java/src/test/java/com/eventstore/dbclient/streams/MetadataTests.java
index 32d963e..b24b294 100644
--- a/db-client-java/src/test/java/com/eventstore/dbclient/streams/MetadataTests.java
+++ b/db-client-java/src/test/java/com/eventstore/dbclient/streams/MetadataTests.java
@@ -13,10 +13,10 @@ public interface MetadataTests extends ConnectionAware {
 
         StreamMetadata metadata = new StreamMetadata();
 
-        metadata.setMaxAge(2);
-        metadata.setCacheControl(15);
-        metadata.setTruncateBefore(1);
-        metadata.setMaxCount(12);
+        metadata.setMaxAge(2L);
+        metadata.setCacheControl(15L);
+        metadata.setTruncateBefore(1L);
+        metadata.setMaxCount(12L);
 
         Acl acl = Acls.newStreamAcl()
                 .addReadRoles("admin")
@@ -49,4 +49,14 @@ public interface MetadataTests extends ConnectionAware {
 
         Assertions.assertEquals(new StreamMetadata(), got);
     }
+
+    @Test
+    default void testReadMetadataAfterStreamDeletion() throws Throwable {
+        EventStoreDBClient client = getDatabase().defaultClient();
+        String streamName = generateName();
+        client.appendToStream(streamName, EventDataBuilder.json("bar", new HashMap<String, Object>()).build()).get();
+
+        client.deleteStream(streamName).get();
+        client.getStreamMetadata(streamName).get();
+    }
 }
\ No newline at end of file