This is an automated email from the ASF dual-hosted git repository.

maciej pushed a commit to branch java-improvements
in repository https://gitbox.apache.org/repos/asf/iggy.git

commit 18029d95fd389f0cb4431306ad0b8b69086ef157
Author: Maciej Modzelewski <[email protected]>
AuthorDate: Mon Feb 2 09:42:04 2026 +0100

    feat(java): improve tests and clean up json mappings
---
 .../blocking/http/MessageMixin.java}               |  31 +--
 .../client/blocking/http/ObjectMapperFactory.java  |  11 +-
 .../blocking/http}/UserHeadersSerializer.java      |   4 +-
 .../java/org/apache/iggy/message/HeaderKey.java    |  41 ++-
 .../java/org/apache/iggy/message/HeaderKind.java   |  16 --
 .../java/org/apache/iggy/message/HeaderValue.java  |  53 ++--
 .../main/java/org/apache/iggy/message/Message.java |  33 +--
 .../java/org/apache/iggy/message/Partitioning.java |   8 +-
 .../org/apache/iggy/serde/Base64Serializer.java    |  35 ---
 .../java/org/apache/iggy/user/Permissions.java     |   7 +-
 .../org/apache/iggy/user/StreamPermissions.java    |   5 +-
 .../iggy/client/blocking/IntegrationTest.java      |   6 +
 .../blocking/http/HeaderKindSerializationTest.java | 120 +++++++++
 .../client/blocking/http/ObjectMapperTest.java     | 286 +++++++++++++++++++++
 .../apache/iggy/serde/BytesDeserializerTest.java   |  76 ------
 15 files changed, 495 insertions(+), 237 deletions(-)

diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/MessageMixin.java
similarity index 56%
copy from 
foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
copy to 
foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/MessageMixin.java
index 878ad8670..9127f079d 100644
--- 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
+++ 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/MessageMixin.java
@@ -17,28 +17,29 @@
  * under the License.
  */
 
-package org.apache.iggy.serde;
+package org.apache.iggy.client.blocking.http;
 
+import com.fasterxml.jackson.annotation.JsonCreator;
 import org.apache.iggy.message.HeaderEntry;
 import org.apache.iggy.message.HeaderKey;
 import org.apache.iggy.message.HeaderValue;
-import tools.jackson.core.JacksonException;
-import tools.jackson.core.JsonGenerator;
-import tools.jackson.databind.SerializationContext;
-import tools.jackson.databind.ValueSerializer;
+import org.apache.iggy.message.Message;
+import org.apache.iggy.message.MessageHeader;
+import tools.jackson.databind.annotation.JsonSerialize;
 
+import java.util.List;
 import java.util.Map;
 
-public class UserHeadersSerializer extends ValueSerializer<Map<HeaderKey, 
HeaderValue>> {
+/**
+ * Jackson mixin for {@link Message} to keep the domain object free of 
serialization annotations.
+ */
+abstract class MessageMixin {
 
-    @Override
-    public void serialize(Map<HeaderKey, HeaderValue> headers, JsonGenerator 
gen, SerializationContext ctxt)
-            throws JacksonException {
-        gen.writeStartArray();
-        for (Map.Entry<HeaderKey, HeaderValue> entry : headers.entrySet()) {
-            ctxt.findValueSerializer(HeaderEntry.class)
-                    .serialize(new HeaderEntry(entry.getKey(), 
entry.getValue()), gen, ctxt);
-        }
-        gen.writeEndArray();
+    @JsonCreator
+    static Message of(MessageHeader header, byte[] payload, List<HeaderEntry> 
userHeaders) {
+        throw new UnsupportedOperationException("Mixin method should not be 
called directly");
     }
+
+    @JsonSerialize(using = UserHeadersSerializer.class)
+    abstract Map<HeaderKey, HeaderValue> userHeaders();
 }
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java
 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java
index cd7a8e9b9..167db5d36 100644
--- 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java
+++ 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/ObjectMapperFactory.java
@@ -21,26 +21,33 @@ package org.apache.iggy.client.blocking.http;
 
 import com.fasterxml.jackson.annotation.JsonSetter;
 import com.fasterxml.jackson.annotation.Nulls;
+import org.apache.iggy.message.Message;
 import tools.jackson.databind.DeserializationFeature;
+import tools.jackson.databind.EnumNamingStrategies;
 import tools.jackson.databind.MapperFeature;
 import tools.jackson.databind.ObjectMapper;
 import tools.jackson.databind.PropertyNamingStrategies;
 import tools.jackson.databind.json.JsonMapper;
 
+import java.util.List;
 import java.util.Map;
 
-public final class ObjectMapperFactory {
+final class ObjectMapperFactory {
 
     private static final ObjectMapper INSTANCE = JsonMapper.builder()
             .enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS)
             .enable(DeserializationFeature.FAIL_ON_NULL_CREATOR_PROPERTIES)
             .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
+            .enumNamingStrategy(EnumNamingStrategies.LOWER_CASE)
             .withConfigOverride(Map.class, map -> 
map.setNullHandling(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)))
+            .withConfigOverride(
+                    List.class, list -> 
list.setNullHandling(JsonSetter.Value.forValueNulls(Nulls.AS_EMPTY)))
+            .addMixIn(Message.class, MessageMixin.class)
             .build();
 
     private ObjectMapperFactory() {}
 
-    public static ObjectMapper getInstance() {
+    static ObjectMapper getInstance() {
         return INSTANCE;
     }
 }
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UserHeadersSerializer.java
similarity index 92%
rename from 
foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
rename to 
foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UserHeadersSerializer.java
index 878ad8670..6f8aa16d6 100644
--- 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/UserHeadersSerializer.java
+++ 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/client/blocking/http/UserHeadersSerializer.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.iggy.serde;
+package org.apache.iggy.client.blocking.http;
 
 import org.apache.iggy.message.HeaderEntry;
 import org.apache.iggy.message.HeaderKey;
@@ -29,7 +29,7 @@ import tools.jackson.databind.ValueSerializer;
 
 import java.util.Map;
 
-public class UserHeadersSerializer extends ValueSerializer<Map<HeaderKey, 
HeaderValue>> {
+class UserHeadersSerializer extends ValueSerializer<Map<HeaderKey, 
HeaderValue>> {
 
     @Override
     public void serialize(Map<HeaderKey, HeaderValue> headers, JsonGenerator 
gen, SerializationContext ctxt)
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java
index ad65857a9..b6acef51a 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKey.java
@@ -19,44 +19,39 @@
 
 package org.apache.iggy.message;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
 
 import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Base64;
 
 public record HeaderKey(HeaderKind kind, byte[] value) {
-    @JsonCreator
-    public static HeaderKey fromJson(@JsonProperty("kind") HeaderKind kind, 
@JsonProperty("value") String base64Value) {
-        byte[] decodedValue = Base64.getDecoder().decode(base64Value);
-        return new HeaderKey(kind, decodedValue);
-    }
 
-    public static HeaderKey fromString(String val) {
-        if (val.isEmpty() || val.length() > 255) {
-            throw new IllegalArgumentException("Value has incorrect size, must 
be between 1 and 255");
+    public static HeaderKey fromString(String value) {
+        if (StringUtils.isBlank(value)) {
+            throw new IllegalArgumentException("Value cannot be null or 
empty");
+        }
+        var bytes = value.getBytes(StandardCharsets.UTF_8);
+        if (bytes.length > 255) {
+            throw new IllegalArgumentException("Value has incorrect size, must 
be between 1 and 255 bytes");
         }
-        return new HeaderKey(HeaderKind.String, 
val.getBytes(StandardCharsets.UTF_8));
+        return new HeaderKey(HeaderKind.String, bytes);
     }
 
     @Override
     public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
         HeaderKey headerKey = (HeaderKey) o;
-        return kind == headerKey.kind && Arrays.equals(value, headerKey.value);
+        return new EqualsBuilder()
+                .append(value, headerKey.value)
+                .append(kind, headerKey.kind)
+                .isEquals();
     }
 
     @Override
     public int hashCode() {
-        int result = kind.hashCode();
-        result = 31 * result + Arrays.hashCode(value);
-        return result;
+        return new HashCodeBuilder(17, 
37).append(kind).append(value).toHashCode();
     }
 
     @Override
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java
index 753a9ee5a..00f024c25 100644
--- 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java
+++ 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderKind.java
@@ -19,39 +19,23 @@
 
 package org.apache.iggy.message;
 
-import com.fasterxml.jackson.annotation.JsonProperty;
 import org.apache.iggy.exception.IggyInvalidArgumentException;
 
 public enum HeaderKind {
-    @JsonProperty("raw")
     Raw(1),
-    @JsonProperty("string")
     String(2),
-    @JsonProperty("bool")
     Bool(3),
-    @JsonProperty("int8")
     Int8(4),
-    @JsonProperty("int16")
     Int16(5),
-    @JsonProperty("int32")
     Int32(6),
-    @JsonProperty("int64")
     Int64(7),
-    @JsonProperty("int128")
     Int128(8),
-    @JsonProperty("uint8")
     Uint8(9),
-    @JsonProperty("uint16")
     Uint16(10),
-    @JsonProperty("uint32")
     Uint32(11),
-    @JsonProperty("uint64")
     Uint64(12),
-    @JsonProperty("uint128")
     Uint128(13),
-    @JsonProperty("float32")
     Float32(14),
-    @JsonProperty("float64")
     Float64(15);
 
     private final int code;
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java
index f4fc6a4c2..937b88fe9 100644
--- 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java
+++ 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/HeaderValue.java
@@ -19,28 +19,26 @@
 
 package org.apache.iggy.message;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.builder.EqualsBuilder;
+import org.apache.commons.lang3.builder.HashCodeBuilder;
 
 import java.nio.ByteBuffer;
 import java.nio.ByteOrder;
 import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
 import java.util.Base64;
 
 public record HeaderValue(HeaderKind kind, byte[] value) {
-    @JsonCreator
-    public static HeaderValue fromJson(
-            @JsonProperty("kind") HeaderKind kind, @JsonProperty("value") 
String base64Value) {
-        byte[] decodedValue = Base64.getDecoder().decode(base64Value);
-        return new HeaderValue(kind, decodedValue);
-    }
 
-    public static HeaderValue fromString(String val) {
-        if (val.isEmpty() || val.length() > 255) {
-            throw new IllegalArgumentException("Value has incorrect size, must 
be between 1 and 255");
+    public static HeaderValue fromString(String value) {
+        if (StringUtils.isBlank(value)) {
+            throw new IllegalArgumentException("Value cannot be null or 
empty");
+        }
+        var bytes = value.getBytes(StandardCharsets.UTF_8);
+        if (bytes.length > 255) {
+            throw new IllegalArgumentException("Value has incorrect size, must 
be between 1 and 255 bytes");
         }
-        return new HeaderValue(HeaderKind.String, 
val.getBytes(StandardCharsets.UTF_8));
+        return new HeaderValue(HeaderKind.String, bytes);
     }
 
     public static HeaderValue fromBool(boolean val) {
@@ -108,7 +106,7 @@ public record HeaderValue(HeaderKind kind, byte[] value) {
 
     public static HeaderValue fromRaw(byte[] val) {
         if (val.length == 0 || val.length > 255) {
-            throw new IllegalArgumentException("Value has incorrect size, must 
be between 1 and 255");
+            throw new IllegalArgumentException("Value has incorrect size, must 
be between 1 and 255 bytes");
         }
         return new HeaderValue(HeaderKind.Raw, val);
     }
@@ -195,27 +193,24 @@ public record HeaderValue(HeaderKind kind, byte[] value) {
     }
 
     @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        HeaderValue that = (HeaderValue) o;
-        return kind == that.kind && Arrays.equals(value, that.value);
+    public String toString() {
+        return toStringValue();
     }
 
     @Override
-    public int hashCode() {
-        int result = kind.hashCode();
-        result = 31 * result + Arrays.hashCode(value);
-        return result;
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        HeaderValue that = (HeaderValue) o;
+        return new EqualsBuilder()
+                .append(value, that.value)
+                .append(kind, that.kind)
+                .isEquals();
     }
 
     @Override
-    public String toString() {
-        return toStringValue();
+    public int hashCode() {
+        return new HashCodeBuilder(17, 
37).append(kind).append(value).toHashCode();
     }
 
     private String toStringValue() {
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java
index 144103d10..de84fc3a2 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Message.java
@@ -19,39 +19,26 @@
 
 package org.apache.iggy.message;
 
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonProperty;
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
-import org.apache.iggy.serde.Base64Serializer;
-import org.apache.iggy.serde.UserHeadersSerializer;
-import tools.jackson.databind.annotation.JsonSerialize;
-
+import javax.annotation.Nullable;
 import java.math.BigInteger;
-import java.util.Base64;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-public record Message(
-        MessageHeader header,
-        @JsonSerialize(using = Base64Serializer.class) byte[] payload,
-        @JsonSerialize(using = UserHeadersSerializer.class) Map<HeaderKey, 
HeaderValue> userHeaders) {
-    @JsonCreator
-    public static Message fromJson(
-            @JsonProperty("header") MessageHeader header,
-            @JsonProperty("payload") String base64Payload,
-            @JsonProperty(value = "user_headers", required = false) 
@JsonSetter(nulls = Nulls.AS_EMPTY)
-                    List<HeaderEntry> userHeadersList) {
-        byte[] decodedPayload = Base64.getDecoder().decode(base64Payload);
+public record Message(MessageHeader header, byte[] payload, Map<HeaderKey, 
HeaderValue> userHeaders) {
+
+    /**
+     * Creates a Message from JSON deserialization. Used by Jackson mixin.
+     */
+    public static Message of(MessageHeader header, byte[] payload, @Nullable 
List<HeaderEntry> userHeaders) {
         Map<HeaderKey, HeaderValue> headersMap = new HashMap<>();
-        if (userHeadersList != null) {
-            for (HeaderEntry entry : userHeadersList) {
+        if (userHeaders != null) {
+            for (HeaderEntry entry : userHeaders) {
                 headersMap.put(entry.key(), entry.value());
             }
         }
-        return new Message(header, decodedPayload, headersMap);
+        return new Message(header, payload, headersMap);
     }
 
     public static Message of(String payload) {
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java
index c451510d2..ca8de8869 100644
--- 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java
+++ 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/message/Partitioning.java
@@ -21,16 +21,12 @@ package org.apache.iggy.message;
 
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.iggy.exception.IggyInvalidArgumentException;
-import org.apache.iggy.serde.Base64Serializer;
-import tools.jackson.databind.annotation.JsonSerialize;
 
 import java.nio.ByteBuffer;
 
-public record Partitioning(
-        PartitioningKind kind,
-        @JsonSerialize(using = Base64Serializer.class) byte[] value) {
+public record Partitioning(PartitioningKind kind, byte[] value) {
     public static Partitioning balanced() {
-        return new Partitioning(PartitioningKind.Balanced, new byte[] {});
+        return new Partitioning(PartitioningKind.Balanced, new byte[]{});
     }
 
     public static Partitioning partitionId(Long id) {
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/Base64Serializer.java
 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/Base64Serializer.java
deleted file mode 100644
index 17bda8b39..000000000
--- 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/serde/Base64Serializer.java
+++ /dev/null
@@ -1,35 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.iggy.serde;
-
-import tools.jackson.core.JacksonException;
-import tools.jackson.core.JsonGenerator;
-import tools.jackson.databind.SerializationContext;
-import tools.jackson.databind.ValueSerializer;
-
-import java.util.Base64;
-
-public class Base64Serializer extends ValueSerializer<byte[]> {
-
-    @Override
-    public void serialize(byte[] value, JsonGenerator gen, 
SerializationContext ctxt) throws JacksonException {
-        gen.writeString(Base64.getEncoder().encodeToString(value));
-    }
-}
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
index f3480e30b..c96dbdc12 100644
--- a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
+++ b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/Permissions.java
@@ -19,11 +19,6 @@
 
 package org.apache.iggy.user;
 
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
-
 import java.util.Map;
 
-public record Permissions(
-        GlobalPermissions global,
-        @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, StreamPermissions> 
streams) {}
+public record Permissions(GlobalPermissions global, Map<Long, 
StreamPermissions> streams) {}
diff --git 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java
 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java
index 1b64e9ada..54c2488d8 100644
--- 
a/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java
+++ 
b/foreign/java/java-sdk/src/main/java/org/apache/iggy/user/StreamPermissions.java
@@ -19,9 +19,6 @@
 
 package org.apache.iggy.user;
 
-import com.fasterxml.jackson.annotation.JsonSetter;
-import com.fasterxml.jackson.annotation.Nulls;
-
 import java.util.Map;
 
 public record StreamPermissions(
@@ -31,4 +28,4 @@ public record StreamPermissions(
         boolean readTopics,
         boolean pollMessages,
         boolean sendMessages,
-        @JsonSetter(nulls = Nulls.AS_EMPTY) Map<Long, TopicPermissions> 
topics) {}
+        Map<Long, TopicPermissions> topics) {}
diff --git 
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java
 
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java
index 8420872d6..46afa9a9c 100644
--- 
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java
+++ 
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/IntegrationTest.java
@@ -19,6 +19,8 @@
 
 package org.apache.iggy.client.blocking;
 
+import com.github.dockerjava.api.model.Capability;
+import com.github.dockerjava.api.model.Ulimit;
 import org.apache.iggy.stream.StreamDetails;
 import org.apache.iggy.topic.CompressionAlgorithm;
 import org.junit.jupiter.api.AfterAll;
@@ -64,6 +66,10 @@ public abstract class IntegrationTest {
                     .withEnv("IGGY_ROOT_PASSWORD", "iggy")
                     .withEnv("IGGY_TCP_ADDRESS", "0.0.0.0:8090")
                     .withEnv("IGGY_HTTP_ADDRESS", "0.0.0.0:3000")
+                    .withCreateContainerCmdModifier(cmd -> cmd.getHostConfig()
+                            .withCapAdd(Capability.SYS_NICE)
+                            .withSecurityOpts(List.of("seccomp:unconfined"))
+                            .withUlimits(List.of(new Ulimit("memlock", -1L, 
-1L))))
                     .withLogConsumer(frame -> 
System.out.print(frame.getUtf8String()));
             iggyServer.start();
         } else {
diff --git 
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HeaderKindSerializationTest.java
 
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HeaderKindSerializationTest.java
new file mode 100644
index 000000000..cb3ff5558
--- /dev/null
+++ 
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/HeaderKindSerializationTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iggy.client.blocking.http;
+
+import org.apache.iggy.message.HeaderKind;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import tools.jackson.databind.ObjectMapper;
+
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class HeaderKindSerializationTest {
+
+    private final ObjectMapper objectMapper = 
ObjectMapperFactory.getInstance();
+
+    @Nested
+    class Serialization {
+
+        @ParameterizedTest
+        
@MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings")
+        void shouldSerializeToLowercase(HeaderKind kind, String expectedJson) {
+            // given
+            // kind provided by parameter
+
+            // when
+            String json = objectMapper.writeValueAsString(kind);
+
+            // then
+            assertThat(json).isEqualTo("\"" + expectedJson + "\"");
+        }
+    }
+
+    @Nested
+    class Deserialization {
+
+        @ParameterizedTest
+        
@MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings")
+        void shouldDeserializeFromLowercase(HeaderKind expectedKind, String 
jsonValue) {
+            // given
+            String json = "\"" + jsonValue + "\"";
+
+            // when
+            HeaderKind result = objectMapper.readValue(json, HeaderKind.class);
+
+            // then
+            assertThat(result).isEqualTo(expectedKind);
+        }
+
+        @ParameterizedTest
+        
@MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings")
+        void shouldDeserializeCaseInsensitive(HeaderKind expectedKind, String 
jsonValue) {
+            // given
+            String json = "\"" + jsonValue.toUpperCase() + "\"";
+
+            // when
+            HeaderKind result = objectMapper.readValue(json, HeaderKind.class);
+
+            // then
+            assertThat(result).isEqualTo(expectedKind);
+        }
+    }
+
+    @Nested
+    class Roundtrip {
+
+        @ParameterizedTest
+        
@MethodSource("org.apache.iggy.client.blocking.http.HeaderKindSerializationTest#headerKindMappings")
+        void shouldRoundtripAllHeaderKinds(HeaderKind kind, String ignored) {
+            // given
+            // kind provided by parameter
+
+            // when
+            String json = objectMapper.writeValueAsString(kind);
+            HeaderKind result = objectMapper.readValue(json, HeaderKind.class);
+
+            // then
+            assertThat(result).isEqualTo(kind);
+        }
+    }
+
+    static Stream<Arguments> headerKindMappings() {
+        return Stream.of(
+                Arguments.of(HeaderKind.Raw, "raw"),
+                Arguments.of(HeaderKind.String, "string"),
+                Arguments.of(HeaderKind.Bool, "bool"),
+                Arguments.of(HeaderKind.Int8, "int8"),
+                Arguments.of(HeaderKind.Int16, "int16"),
+                Arguments.of(HeaderKind.Int32, "int32"),
+                Arguments.of(HeaderKind.Int64, "int64"),
+                Arguments.of(HeaderKind.Int128, "int128"),
+                Arguments.of(HeaderKind.Uint8, "uint8"),
+                Arguments.of(HeaderKind.Uint16, "uint16"),
+                Arguments.of(HeaderKind.Uint32, "uint32"),
+                Arguments.of(HeaderKind.Uint64, "uint64"),
+                Arguments.of(HeaderKind.Uint128, "uint128"),
+                Arguments.of(HeaderKind.Float32, "float32"),
+                Arguments.of(HeaderKind.Float64, "float64"));
+    }
+}
diff --git 
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/ObjectMapperTest.java
 
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/ObjectMapperTest.java
new file mode 100644
index 000000000..eefda1659
--- /dev/null
+++ 
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/client/blocking/http/ObjectMapperTest.java
@@ -0,0 +1,286 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.iggy.client.blocking.http;
+
+import org.apache.iggy.message.Message;
+import org.apache.iggy.message.PolledMessages;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import tools.jackson.databind.ObjectMapper;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ObjectMapperTest {
+
+    private final ObjectMapper objectMapper = 
ObjectMapperFactory.getInstance();
+
+    @Nested
+    class Deserialization {
+
+        @Nested
+        @DisplayName("PolledMessages")
+        class PolledMessagesDeserialization {
+
+            @Test
+            void shouldDeserializePolledMessagesWithEmptyUserHeaders() {
+                // given
+                String json = """
+                        {
+                          "partition_id": 1,
+                          "current_offset": 10,
+                          "count": 1,
+                          "messages": [
+                            {
+                              "header": {
+                                "checksum": 0,
+                                "id": 42,
+                                "offset": 0,
+                                "timestamp": 0,
+                                "origin_timestamp": 1000,
+                                "user_headers_length": 0,
+                                "payload_length": 4
+                              },
+                              "payload": "dGVzdA==",
+                              "user_headers": []
+                            }
+                          ]
+                        }
+                        """;
+
+                // when
+                var polledMessages = objectMapper.readValue(json, 
PolledMessages.class);
+
+                // then
+                assertThat(polledMessages).isNotNull();
+                assertThat(polledMessages.messages()).hasSize(1);
+                
assertThat(polledMessages.messages().get(0).userHeaders()).isEmpty();
+            }
+
+            @Test
+            void shouldDeserializePolledMessagesWithUserHeaders() {
+                // given
+                String json = """
+                        {
+                          "partition_id": 1,
+                          "current_offset": 10,
+                          "count": 1,
+                          "messages": [
+                            {
+                              "header": {
+                                "checksum": 0,
+                                "id": 42,
+                                "offset": 0,
+                                "timestamp": 0,
+                                "origin_timestamp": 1000,
+                                "user_headers_length": 62,
+                                "payload_length": 4
+                              },
+                              "payload": "dGVzdA==",
+                              "user_headers": [
+                                {
+                                  "key": {"kind": "string", "value": 
"Y29udGVudC10eXBl"},
+                                  "value": {"kind": "string", "value": 
"dGV4dC9wbGFpbg=="}
+                                }
+                              ]
+                            }
+                          ]
+                        }
+                        """;
+
+                // when
+                var polledMessages = objectMapper.readValue(json, 
PolledMessages.class);
+
+                // then
+                assertThat(polledMessages).isNotNull();
+                assertThat(polledMessages.messages()).hasSize(1);
+                var headers = polledMessages.messages().get(0).userHeaders();
+                assertThat(headers).hasSize(1);
+                var header = headers.entrySet().iterator().next();
+                
assertThat(header.getKey().toString()).isEqualTo("content-type");
+                
assertThat(header.getValue().toString()).isEqualTo("text/plain");
+            }
+        }
+
+        @Nested
+        @DisplayName("Payload")
+        class PayloadDeserialization {
+
+            @Test
+            void shouldDeserializeBase64EncodedPayloadToBytes() {
+                // given
+                String expectedPayload = "test";
+                String base64Payload =
+                        
Base64.getEncoder().encodeToString(expectedPayload.getBytes(StandardCharsets.UTF_8));
+                String json = createMessageJson(base64Payload);
+
+                // when
+                var polledMessages = objectMapper.readValue(json, 
PolledMessages.class);
+
+                // then
+                byte[] actualPayload = 
polledMessages.messages().get(0).payload();
+                
assertThat(actualPayload).isEqualTo(expectedPayload.getBytes(StandardCharsets.UTF_8));
+            }
+
+            @Test
+            void shouldDeserializeEmptyPayload() {
+                // given
+                String base64Payload = Base64.getEncoder().encodeToString(new 
byte[0]);
+                String json = createMessageJson(base64Payload);
+
+                // when
+                var polledMessages = objectMapper.readValue(json, 
PolledMessages.class);
+
+                // then
+                byte[] actualPayload = 
polledMessages.messages().get(0).payload();
+                assertThat(actualPayload).isEmpty();
+            }
+
+            @Test
+            void shouldDeserializeBinaryPayload() {
+                // given
+                byte[] binaryData = new byte[] {0x00, 0x01, 0x02, (byte) 0xFF, 
(byte) 0xFE};
+                String base64Payload = 
Base64.getEncoder().encodeToString(binaryData);
+                String json = createMessageJson(base64Payload);
+
+                // when
+                var polledMessages = objectMapper.readValue(json, 
PolledMessages.class);
+
+                // then
+                byte[] actualPayload = 
polledMessages.messages().get(0).payload();
+                assertThat(actualPayload).isEqualTo(binaryData);
+            }
+
+            private String createMessageJson(String base64Payload) {
+                return """
+                        {
+                          "partition_id": 1,
+                          "current_offset": 0,
+                          "count": 1,
+                          "messages": [
+                            {
+                              "header": {
+                                "checksum": 0,
+                                "id": 1,
+                                "offset": 0,
+                                "timestamp": 0,
+                                "origin_timestamp": 0,
+                                "user_headers_length": 0,
+                                "payload_length": 4
+                              },
+                              "payload": "%s",
+                              "user_headers": []
+                            }
+                          ]
+                        }
+                        """.formatted(base64Payload);
+            }
+        }
+    }
+
+    @Nested
+    class Serialization {
+
+        @Nested
+        @DisplayName("Payload")
+        class PayloadSerialization {
+
+            @Test
+            void shouldSerializePayloadToBase64() {
+                // given
+                String payloadContent = "test";
+                Message message = Message.of(payloadContent);
+
+                // when
+                String json = objectMapper.writeValueAsString(message);
+
+                // then
+                String expectedBase64 =
+                        
Base64.getEncoder().encodeToString(payloadContent.getBytes(StandardCharsets.UTF_8));
+                assertThat(json).contains("\"payload\":\"" + expectedBase64 + 
"\"");
+            }
+
+            @Test
+            void shouldSerializeEmptyPayloadToBase64() {
+                // given
+                String payloadContent = "";
+                Message message = Message.of(payloadContent);
+
+                // when
+                String json = objectMapper.writeValueAsString(message);
+
+                // then
+                String expectedBase64 = Base64.getEncoder().encodeToString(new 
byte[0]);
+                assertThat(json).contains("\"payload\":\"" + expectedBase64 + 
"\"");
+            }
+
+            @Test
+            void shouldSerializeBinaryPayloadToBase64() {
+                // given
+                byte[] binaryData = new byte[] {0x00, 0x01, 0x02, (byte) 0xFF, 
(byte) 0xFE};
+                Message message = Message.of("placeholder");
+                Message binaryMessage = new Message(message.header(), 
binaryData, message.userHeaders());
+
+                // when
+                String json = objectMapper.writeValueAsString(binaryMessage);
+
+                // then
+                String expectedBase64 = 
Base64.getEncoder().encodeToString(binaryData);
+                assertThat(json).contains("\"payload\":\"" + expectedBase64 + 
"\"");
+            }
+        }
+    }
+
+    @Nested
+    class Roundtrip {
+
+        @Test
+        void shouldRoundtripTextPayload() {
+            // given
+            String payloadContent = "Hello, World!";
+            Message originalMessage = Message.of(payloadContent);
+
+            // when
+            String json = objectMapper.writeValueAsString(originalMessage);
+            Message deserializedMessage = objectMapper.readValue(json, 
Message.class);
+
+            // then
+            
assertThat(deserializedMessage.payload()).isEqualTo(originalMessage.payload());
+        }
+
+        @Test
+        void shouldRoundtripBinaryPayload() {
+            // given
+            byte[] binaryData = new byte[] {0x00, 0x01, 0x02, (byte) 0x80, 
(byte) 0xFF};
+            Message originalMessage = Message.of("placeholder");
+            Message binaryMessage = new Message(originalMessage.header(), 
binaryData, originalMessage.userHeaders());
+
+            // when
+            String json = objectMapper.writeValueAsString(binaryMessage);
+            Message deserializedMessage = objectMapper.readValue(json, 
Message.class);
+
+            // then
+            assertThat(deserializedMessage.payload()).isEqualTo(binaryData);
+        }
+    }
+}
diff --git 
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
 
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
index 36a6d5ea3..496a428e5 100644
--- 
a/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
+++ 
b/foreign/java/java-sdk/src/test/java/org/apache/iggy/serde/BytesDeserializerTest.java
@@ -752,80 +752,4 @@ class BytesDeserializerTest {
             assertThat(tokenInfo.expiryAt()).isEmpty();
         }
     }
-
-    @Nested
-    class JsonDeserialization {
-
-        private static final tools.jackson.databind.ObjectMapper MAPPER =
-                
org.apache.iggy.client.blocking.http.ObjectMapperFactory.getInstance();
-
-        @Test
-        void shouldDeserializePolledMessagesWithEmptyUserHeaders() throws 
Exception {
-            String json = """
-                {
-                  "partition_id": 1,
-                  "current_offset": 10,
-                  "count": 1,
-                  "messages": [
-                    {
-                      "header": {
-                        "checksum": 0,
-                        "id": 42,
-                        "offset": 0,
-                        "timestamp": 0,
-                        "origin_timestamp": 1000,
-                        "user_headers_length": 0,
-                        "payload_length": 4
-                      },
-                      "payload": "dGVzdA==",
-                      "user_headers": []
-                    }
-                  ]
-                }
-                """;
-
-            var polledMessages = MAPPER.readValue(json, 
org.apache.iggy.message.PolledMessages.class);
-
-            assertThat(polledMessages).isNotNull();
-            assertThat(polledMessages.messages()).hasSize(1);
-            
assertThat(polledMessages.messages().get(0).userHeaders()).isEmpty();
-        }
-
-        @Test
-        void shouldDeserializePolledMessagesWithUserHeaders() throws Exception 
{
-            String json = """
-                {
-                  "partition_id": 1,
-                  "current_offset": 10,
-                  "count": 1,
-                  "messages": [
-                    {
-                      "header": {
-                        "checksum": 0,
-                        "id": 42,
-                        "offset": 0,
-                        "timestamp": 0,
-                        "origin_timestamp": 1000,
-                        "user_headers_length": 62,
-                        "payload_length": 4
-                      },
-                      "payload": "dGVzdA==",
-                      "user_headers": [
-                        {
-                          "key": {"kind": "string", "value": 
"Y29udGVudC10eXBl"},
-                          "value": {"kind": "string", "value": 
"dGV4dC9wbGFpbg=="}
-                        }
-                      ]
-                    }
-                  ]
-                }
-                """;
-
-            var polledMessages = MAPPER.readValue(json, 
org.apache.iggy.message.PolledMessages.class);
-
-            assertThat(polledMessages).isNotNull();
-            assertThat(polledMessages.messages()).hasSize(1);
-            
assertThat(polledMessages.messages().get(0).userHeaders()).hasSize(1);
-        }
-    }
 }


Reply via email to