You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by sd...@apache.org on 2022/01/18 14:54:03 UTC

[ignite-3] branch main updated: IGNITE-16240 Support putFields()+writeFields() and readFields() in User Object Serialization

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

sdanilov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new a7d78f9  IGNITE-16240 Support putFields()+writeFields() and readFields() in User Object Serialization
a7d78f9 is described below

commit a7d78f9c597bcab939b89334525f69041d61d1c4
Author: Roman Puchkovskiy <ro...@gmail.com>
AuthorDate: Mon Jan 17 21:11:28 2022 +0400

    IGNITE-16240 Support putFields()+writeFields() and readFields() in User Object Serialization
---
 .../network/serialization/ClassDescriptor.java     | 159 ++++++++++++-
 .../network/serialization/FieldDescriptor.java     |  21 ++
 .../internal/network/serialization/Primitives.java |  52 ++++
 .../network/serialization/marshal/Bits.java        | 103 ++++++++
 .../marshal/ExternalizableMarshaller.java          |  23 +-
 .../marshal/StructuredObjectMarshaller.java        |  17 +-
 .../marshal/UosObjectInputStream.java              | 131 +++++++++++
 .../marshal/UosObjectOutputStream.java             | 138 ++++++++++-
 .../network/serialization/PrimitivesTest.java      |  58 +++++
 ...shallerWithSerializableOverrideStreamsTest.java | 262 ++++++++++++++++++++-
 .../network/serialization/marshal/IntHolder.java   |  29 +++
 11 files changed, 965 insertions(+), 28 deletions(-)

diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptor.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptor.java
index 247bf09..97602af 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptor.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptor.java
@@ -17,8 +17,14 @@
 
 package org.apache.ignite.internal.network.serialization;
 
+import static java.util.stream.Collectors.toUnmodifiableMap;
+
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import java.lang.reflect.Modifier;
 import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -59,6 +65,21 @@ public class ClassDescriptor {
      */
     private final boolean isFinal;
 
+    /** Total number of bytes needed to store all primitive fields. */
+    private final int primitiveFieldsDataSize;
+    /** Total number of non-primitive fields. */
+    private final int objectFieldsCount;
+
+    private Map<String, FieldDescriptor> fieldsByName;
+    /**
+     * Offsets into primitive fields data array (which has size {@link #primitiveFieldsDataSize}).
+     * This array is a byte array containing data of all the primitive fields of an object.
+     * (Not to be confused with the offsets used in the context of {@link sun.misc.Unsafe}).
+     */
+    private Object2IntMap<String> primitiveFieldDataOffsets;
+    /** Indices of non-primitive fields in the object fields array. */
+    private Object2IntMap<String> objectFieldIndices;
+
     private final SpecialSerializationMethods serializationMethods;
 
     /**
@@ -78,9 +99,28 @@ public class ClassDescriptor {
         this.serialization = serialization;
         this.isFinal = Modifier.isFinal(clazz.getModifiers());
 
+        primitiveFieldsDataSize = computePrimitiveFieldsDataSize(fields);
+        objectFieldsCount = computeObjectFieldsCount(fields);
+
         serializationMethods = new SpecialSerializationMethodsImpl(this);
     }
 
+    private static int computePrimitiveFieldsDataSize(List<FieldDescriptor> fields) {
+        int accumulatedBytes = 0;
+        for (FieldDescriptor fieldDesc : fields) {
+            if (fieldDesc.isPrimitive()) {
+                accumulatedBytes += Primitives.widthInBytes(fieldDesc.clazz());
+            }
+        }
+        return accumulatedBytes;
+    }
+
+    private static int computeObjectFieldsCount(List<FieldDescriptor> fields) {
+        return (int) fields.stream()
+                .filter(fieldDesc -> !fieldDesc.isPrimitive())
+                .count();
+    }
+
     /**
      * Returns descriptor id.
      *
@@ -288,14 +328,6 @@ public class ClassDescriptor {
         return serializationMethods;
     }
 
-    @Override
-    public String toString() {
-        return "ClassDescriptor{"
-                + "className='" + className() + '\''
-                + ", descriptorId=" + descriptorId
-                + '}';
-    }
-
     /**
      * Returns {@code true} if this descriptor describes same class as the given descriptor.
      *
@@ -305,4 +337,115 @@ public class ClassDescriptor {
     public boolean describesSameClass(ClassDescriptor other) {
         return other.clazz() == clazz();
     }
+
+    /**
+     * Returns total number of bytes needed to store all primitive fields.
+     *
+     * @return total number of bytes needed to store all primitive fields
+     */
+    public int primitiveFieldsDataSize() {
+        return primitiveFieldsDataSize;
+    }
+
+    /**
+     * Returns total number of object (i.e. non-primitive) fields.
+     *
+     * @return total number of object (i.e. non-primitive) fields
+     */
+    public int objectFieldsCount() {
+        return objectFieldsCount;
+    }
+
+    /**
+     * Return offset into primitive fields data (which has size {@link #primitiveFieldsDataSize()}).
+     * These are different from the offsets used in the context of {@link sun.misc.Unsafe}.
+     *
+     * @param fieldName    primitive field name
+     * @param requiredType field type
+     * @return offset into primitive fields data
+     */
+    public int primitiveFieldDataOffset(String fieldName, Class<?> requiredType) {
+        assert requiredType.isPrimitive();
+
+        if (fieldsByName == null) {
+            fieldsByName = fieldsByNameMap(fields);
+        }
+
+        FieldDescriptor fieldDesc = fieldsByName.get(fieldName);
+        if (fieldDesc == null) {
+            throw new IllegalStateException("Did not find a field with name " + fieldName);
+        }
+        if (fieldDesc.clazz() != requiredType) {
+            throw new IllegalStateException("Field " + fieldName + " has type " + fieldDesc.clazz()
+                    + ", but it was used as " + requiredType);
+        }
+
+        if (primitiveFieldDataOffsets == null) {
+            primitiveFieldDataOffsets = primitiveFieldDataOffsetsMap(fields);
+        }
+
+        assert primitiveFieldDataOffsets.containsKey(fieldName);
+
+        return primitiveFieldDataOffsets.getInt(fieldName);
+    }
+
+    private static Map<String, FieldDescriptor> fieldsByNameMap(List<FieldDescriptor> fields) {
+        return fields.stream()
+                .collect(toUnmodifiableMap(FieldDescriptor::name, Function.identity()));
+    }
+
+    private static Object2IntMap<String> primitiveFieldDataOffsetsMap(List<FieldDescriptor> fields) {
+        Object2IntMap<String> map = new Object2IntOpenHashMap<>();
+
+        int accumulatedOffset = 0;
+        for (FieldDescriptor fieldDesc : fields) {
+            if (fieldDesc.isPrimitive()) {
+                map.put(fieldDesc.name(), accumulatedOffset);
+                accumulatedOffset += Primitives.widthInBytes(fieldDesc.clazz());
+            }
+        }
+
+        return map;
+    }
+
+    /**
+     * Returns index of a non-primitive (i.e. object) field in the object fields array.
+     *
+     * @param fieldName object field name
+     * @return index of a non-primitive (i.e. object) field in the object fields array
+     */
+    public int objectFieldIndex(String fieldName) {
+        if (objectFieldIndices == null) {
+            objectFieldIndices = computeObjectFieldIndices(fields);
+        }
+
+        if (!objectFieldIndices.containsKey(fieldName)) {
+            throw new IllegalStateException("Did not find an object field with name " + fieldName);
+        }
+
+        return objectFieldIndices.getInt(fieldName);
+    }
+
+    private Object2IntMap<String> computeObjectFieldIndices(List<FieldDescriptor> fields) {
+        Object2IntMap<String> map = new Object2IntOpenHashMap<>();
+
+        int currentIndex = 0;
+        for (FieldDescriptor fieldDesc : fields) {
+            if (!fieldDesc.isPrimitive()) {
+                map.put(fieldDesc.name(), currentIndex);
+                currentIndex++;
+            }
+        }
+
+        return map;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "ClassDescriptor{"
+                + "className='" + className() + '\''
+                + ", descriptorId=" + descriptorId
+                + '}';
+    }
 }
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldDescriptor.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldDescriptor.java
index e3e4687..826a40b 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldDescriptor.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldDescriptor.java
@@ -100,6 +100,27 @@ public class FieldDescriptor {
     }
 
     /**
+     * Returns {@code true} if this field has a primitive type.
+     *
+     * @return {@code true} if this field has a primitive type
+     */
+    public boolean isPrimitive() {
+        return clazz.isPrimitive();
+    }
+
+    /**
+     * Returns width in bytes (that is, how many bytes a value of the field type takes) of the field type.
+     * If the field type is not primitive, throws an exception.
+     *
+     * @return width in bytes
+     */
+    public int primitiveWidthInBytes() {
+        assert isPrimitive();
+
+        return Primitives.widthInBytes(clazz);
+    }
+
+    /**
      * Returns {@link FieldAccessor} for this field.
      *
      * @return {@link FieldAccessor} for this field
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/Primitives.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/Primitives.java
new file mode 100644
index 0000000..969ab1c
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/Primitives.java
@@ -0,0 +1,52 @@
+/*
+ * 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.ignite.internal.network.serialization;
+
+/**
+ * Utils to work with primitives.
+ */
+public class Primitives {
+    /**
+     * Returns number of bytes a value of the given primtive type takes (1 for byte, 8 for long and so on).
+     *
+     * @param clazz primitive type
+     * @return number of bytes
+     * @throws IllegalArgumentException if the passed type is not primitive
+     */
+    public static int widthInBytes(Class<?> clazz) {
+        if (clazz == byte.class) {
+            return Byte.BYTES;
+        } else if (clazz == short.class) {
+            return Short.BYTES;
+        } else if (clazz == int.class) {
+            return Integer.BYTES;
+        } else if (clazz == long.class) {
+            return Long.BYTES;
+        } else if (clazz == float.class) {
+            return Float.BYTES;
+        } else if (clazz == double.class) {
+            return Double.BYTES;
+        } else if (clazz == char.class) {
+            return Character.BYTES;
+        } else if (clazz == boolean.class) {
+            return 1;
+        } else {
+            throw new IllegalArgumentException(clazz + " is not primitive");
+        }
+    }
+}
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/Bits.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/Bits.java
new file mode 100644
index 0000000..ae5085c
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/Bits.java
@@ -0,0 +1,103 @@
+/*
+ * 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.ignite.internal.network.serialization.marshal;
+
+/**
+ * Code for packing/unpacking primitive values to/from a byte array at specific offsets.
+ */
+class Bits {
+    static boolean getBoolean(byte[] b, int off) {
+        return b[off] != 0;
+    }
+
+    static char getChar(byte[] b, int off) {
+        return (char) ((b[off + 1] & 0xFF)
+                + (b[off] << 8));
+    }
+
+    static short getShort(byte[] b, int off) {
+        return (short) ((b[off + 1] & 0xFF)
+                + (b[off] << 8));
+    }
+
+    static int getInt(byte[] b, int off) {
+        return ((b[off + 3] & 0xFF))
+                + ((b[off + 2] & 0xFF) << 8)
+                + ((b[off + 1] & 0xFF) << 16)
+                + ((b[off]) << 24);
+    }
+
+    static float getFloat(byte[] b, int off) {
+        return Float.intBitsToFloat(getInt(b, off));
+    }
+
+    static long getLong(byte[] b, int off) {
+        return ((b[off + 7] & 0xFFL))
+                + ((b[off + 6] & 0xFFL) << 8)
+                + ((b[off + 5] & 0xFFL) << 16)
+                + ((b[off + 4] & 0xFFL) << 24)
+                + ((b[off + 3] & 0xFFL) << 32)
+                + ((b[off + 2] & 0xFFL) << 40)
+                + ((b[off + 1] & 0xFFL) << 48)
+                + (((long) b[off]) << 56);
+    }
+
+    static double getDouble(byte[] b, int off) {
+        return Double.longBitsToDouble(getLong(b, off));
+    }
+
+    static void putBoolean(byte[] b, int off, boolean val) {
+        b[off] = (byte) (val ? 1 : 0);
+    }
+
+    static void putChar(byte[] b, int off, char val) {
+        b[off + 1] = (byte) (val);
+        b[off] = (byte) (val >>> 8);
+    }
+
+    static void putShort(byte[] b, int off, short val) {
+        b[off + 1] = (byte) (val);
+        b[off] = (byte) (val >>> 8);
+    }
+
+    static void putInt(byte[] b, int off, int val) {
+        b[off + 3] = (byte) (val);
+        b[off + 2] = (byte) (val >>> 8);
+        b[off + 1] = (byte) (val >>> 16);
+        b[off] = (byte) (val >>> 24);
+    }
+
+    static void putFloat(byte[] b, int off, float val) {
+        putInt(b, off, Float.floatToIntBits(val));
+    }
+
+    static void putLong(byte[] b, int off, long val) {
+        b[off + 7] = (byte) (val);
+        b[off + 6] = (byte) (val >>> 8);
+        b[off + 5] = (byte) (val >>> 16);
+        b[off + 4] = (byte) (val >>> 24);
+        b[off + 3] = (byte) (val >>> 32);
+        b[off + 2] = (byte) (val >>> 40);
+        b[off + 1] = (byte) (val >>> 48);
+        b[off] = (byte) (val >>> 56);
+    }
+
+    static void putDouble(byte[] b, int off, double val) {
+        putLong(b, off, Double.doubleToLongBits(val));
+    }
+}
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/ExternalizableMarshaller.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/ExternalizableMarshaller.java
index ac18624..77a706a 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/ExternalizableMarshaller.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/ExternalizableMarshaller.java
@@ -21,7 +21,6 @@ import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.Externalizable;
 import java.io.IOException;
-import java.io.ObjectInputStream;
 import org.apache.ignite.internal.network.serialization.ClassDescriptor;
 
 /**
@@ -53,12 +52,18 @@ class ExternalizableMarshaller {
 
     private void externalizeTo(Externalizable externalizable, DataOutputStream output, MarshallingContext context)
             throws IOException {
-        context.endWritingWithWriteObject();
-
         // Do not close the stream yet!
         UosObjectOutputStream oos = context.objectOutputStream(output, typedValueWriter, defaultFieldsReaderWriter);
-        externalizable.writeExternal(oos);
-        oos.flush();
+
+        UosObjectOutputStream.UosPutField oldPut = oos.replaceCurrentPutFieldWithNull();
+        context.endWritingWithWriteObject();
+
+        try {
+            externalizable.writeExternal(oos);
+            oos.flush();
+        } finally {
+            oos.restoreCurrentPutFieldTo(oldPut);
+        }
     }
 
     @SuppressWarnings("unchecked")
@@ -72,14 +77,18 @@ class ExternalizableMarshaller {
 
     <T extends Externalizable> void fillExternalizableFrom(DataInputStream input, T object, UnmarshallingContext context)
             throws IOException, UnmarshalException {
+        // Do not close the stream yet!
+        UosObjectInputStream ois = context.objectInputStream(input, valueReader, defaultFieldsReaderWriter);
+
+        UosObjectInputStream.UosGetField oldGet = ois.replaceCurrentGetFieldWithNull();
         context.endReadingWithReadObject();
 
-        // Do not close the stream yet!
-        ObjectInputStream ois = context.objectInputStream(input, valueReader, defaultFieldsReaderWriter);
         try {
             object.readExternal(ois);
         } catch (ClassNotFoundException e) {
             throw new UnmarshalException("Cannot unmarshal due to a missing class", e);
+        } finally {
+            ois.restoreCurrentGetFieldTo(oldGet);
         }
     }
 }
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/StructuredObjectMarshaller.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/StructuredObjectMarshaller.java
index 2e6278a..50dc4af 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/StructuredObjectMarshaller.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/StructuredObjectMarshaller.java
@@ -20,7 +20,6 @@ package org.apache.ignite.internal.network.serialization.marshal;
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.IOException;
-import java.io.ObjectInputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -30,6 +29,8 @@ import org.apache.ignite.internal.network.serialization.FieldAccessor;
 import org.apache.ignite.internal.network.serialization.FieldDescriptor;
 import org.apache.ignite.internal.network.serialization.IdIndexedDescriptors;
 import org.apache.ignite.internal.network.serialization.SpecialMethodInvocationException;
+import org.apache.ignite.internal.network.serialization.marshal.UosObjectInputStream.UosGetField;
+import org.apache.ignite.internal.network.serialization.marshal.UosObjectOutputStream.UosPutField;
 
 /**
  * (Un)marshals objects that have structure (fields). These are {@link java.io.Serializable}s
@@ -93,17 +94,20 @@ class StructuredObjectMarshaller implements DefaultFieldsReaderWriter {
 
     private void writeWithWriteObject(Object object, ClassDescriptor descriptor, DataOutputStream output, MarshallingContext context)
             throws IOException, MarshalException {
+        // Do not close the stream yet!
+        UosObjectOutputStream oos = context.objectOutputStream(output, valueWriter, this);
+
+        UosPutField oldPut = oos.replaceCurrentPutFieldWithNull();
         context.startWritingWithWriteObject(object, descriptor);
 
         try {
-            // Do not close the stream yet!
-            UosObjectOutputStream oos = context.objectOutputStream(output, valueWriter, this);
             descriptor.serializationMethods().writeObject(object, oos);
             oos.flush();
         } catch (SpecialMethodInvocationException e) {
             throw new MarshalException("Cannot invoke writeObject()", e);
         } finally {
             context.endWritingWithWriteObject();
+            oos.restoreCurrentPutFieldTo(oldPut);
         }
     }
 
@@ -191,16 +195,19 @@ class StructuredObjectMarshaller implements DefaultFieldsReaderWriter {
             ClassDescriptor descriptor,
             UnmarshallingContext context
     ) throws IOException, UnmarshalException {
+        // Do not close the stream yet!
+        UosObjectInputStream ois = context.objectInputStream(input, valueReader, this);
+
+        UosGetField oldGet = ois.replaceCurrentGetFieldWithNull();
         context.startReadingWithReadObject(object, descriptor);
 
         try {
-            // Do not close the stream yet!
-            ObjectInputStream ois = context.objectInputStream(input, valueReader, this);
             descriptor.serializationMethods().readObject(object, ois);
         } catch (SpecialMethodInvocationException e) {
             throw new UnmarshalException("Cannot invoke readObject()", e);
         } finally {
             context.endReadingWithReadObject();
+            ois.restoreCurrentGetFieldTo(oldGet);
         }
     }
 
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UosObjectInputStream.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UosObjectInputStream.java
index 85e198a..b679d3a 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UosObjectInputStream.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UosObjectInputStream.java
@@ -17,9 +17,14 @@
 
 package org.apache.ignite.internal.network.serialization.marshal;
 
+import java.io.DataInput;
 import java.io.DataInputStream;
 import java.io.IOException;
 import java.io.ObjectInputStream;
+import java.io.ObjectStreamClass;
+import org.apache.ignite.internal.network.serialization.ClassDescriptor;
+import org.apache.ignite.internal.network.serialization.FieldDescriptor;
+import org.apache.ignite.internal.network.serialization.Primitives;
 
 /**
  * {@link ObjectInputStream} specialization used by User Object Serialization.
@@ -30,6 +35,8 @@ class UosObjectInputStream extends ObjectInputStream {
     private final DefaultFieldsReaderWriter defaultFieldsReaderWriter;
     private final UnmarshallingContext context;
 
+    private UosGetField currentGet;
+
     UosObjectInputStream(
             DataInputStream input,
             ValueReader<Object> valueReader,
@@ -184,6 +191,16 @@ class UosObjectInputStream extends ObjectInputStream {
 
     /** {@inheritDoc} */
     @Override
+    public GetField readFields() throws IOException {
+        if (currentGet == null) {
+            currentGet = new UosGetField(context.descriptorOfObjectCurrentlyReadWithReadObject());
+            currentGet.readFields();
+        }
+        return currentGet;
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public int available() throws IOException {
         return input.available();
     }
@@ -197,5 +214,119 @@ class UosObjectInputStream extends ObjectInputStream {
     /** {@inheritDoc} */
     @Override
     public void close() throws IOException {
+        // no-op
+    }
+
+    UosGetField replaceCurrentGetFieldWithNull() {
+        UosGetField oldGet = currentGet;
+        currentGet = null;
+        return oldGet;
+    }
+
+    void restoreCurrentGetFieldTo(UosGetField newGet) {
+        currentGet = newGet;
+    }
+
+    class UosGetField extends GetField {
+        private final DataInput input = UosObjectInputStream.this;
+        private final ClassDescriptor descriptor;
+
+        private final byte[] primitiveFieldsData;
+        private final Object[] objectFieldVals;
+
+        private UosGetField(ClassDescriptor currentObjectDescriptor) {
+            this.descriptor = currentObjectDescriptor;
+
+            primitiveFieldsData = new byte[currentObjectDescriptor.primitiveFieldsDataSize()];
+            objectFieldVals = new Object[currentObjectDescriptor.objectFieldsCount()];
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public ObjectStreamClass getObjectStreamClass() {
+            return ObjectStreamClass.lookupAny(descriptor.clazz());
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean defaulted(String name) throws IOException {
+            // TODO: IGNITE-15948 - actually take into account whether it's defaulted or not
+            return false;
+        }
+
+        // TODO: IGNITE-15948 - return default values if the field exists locally but not in the stream being parsed
+
+        /** {@inheritDoc} */
+        @Override
+        public boolean get(String name, boolean val) throws IOException {
+            return Bits.getBoolean(primitiveFieldsData, primitiveFieldDataOffset(name, boolean.class));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public byte get(String name, byte val) throws IOException {
+            return primitiveFieldsData[primitiveFieldDataOffset(name, byte.class)];
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public char get(String name, char val) throws IOException {
+            return Bits.getChar(primitiveFieldsData, primitiveFieldDataOffset(name, char.class));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public short get(String name, short val) throws IOException {
+            return Bits.getShort(primitiveFieldsData, primitiveFieldDataOffset(name, short.class));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int get(String name, int val) throws IOException {
+            return Bits.getInt(primitiveFieldsData, primitiveFieldDataOffset(name, int.class));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public long get(String name, long val) throws IOException {
+            return Bits.getLong(primitiveFieldsData, primitiveFieldDataOffset(name, long.class));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public float get(String name, float val) throws IOException {
+            return Bits.getFloat(primitiveFieldsData, primitiveFieldDataOffset(name, float.class));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public double get(String name, double val) throws IOException {
+            return Bits.getDouble(primitiveFieldsData, primitiveFieldDataOffset(name, double.class));
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Object get(String name, Object val) throws IOException {
+            return objectFieldVals[descriptor.objectFieldIndex(name)];
+        }
+
+        private int primitiveFieldDataOffset(String fieldName, Class<?> requiredType) {
+            return descriptor.primitiveFieldDataOffset(fieldName, requiredType);
+        }
+
+        private void readFields() throws IOException {
+            int objectFieldIndex = 0;
+
+            for (FieldDescriptor fieldDesc : descriptor.fields()) {
+                if (fieldDesc.isPrimitive()) {
+                    int offset = descriptor.primitiveFieldDataOffset(fieldDesc.name(), fieldDesc.clazz());
+                    int length = Primitives.widthInBytes(fieldDesc.clazz());
+                    input.readFully(primitiveFieldsData, offset, length);
+                } else {
+                    objectFieldVals[objectFieldIndex] = doReadObject();
+                    objectFieldIndex++;
+                }
+            }
+        }
     }
 }
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UosObjectOutputStream.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UosObjectOutputStream.java
index ad8e7f4..1334842 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UosObjectOutputStream.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UosObjectOutputStream.java
@@ -21,7 +21,12 @@ import static org.apache.ignite.internal.network.serialization.marshal.ObjectCla
 
 import java.io.DataOutputStream;
 import java.io.IOException;
+import java.io.NotActiveException;
+import java.io.ObjectOutput;
 import java.io.ObjectOutputStream;
+import org.apache.ignite.internal.network.serialization.ClassDescriptor;
+import org.apache.ignite.internal.network.serialization.FieldDescriptor;
+import org.apache.ignite.internal.network.serialization.Primitives;
 
 /**
  * {@link ObjectOutputStream} specialization used by User Object Serialization.
@@ -32,6 +37,8 @@ class UosObjectOutputStream extends ObjectOutputStream {
     private final DefaultFieldsReaderWriter defaultFieldsReaderWriter;
     private final MarshallingContext context;
 
+    private UosPutField currentPut;
+
     UosObjectOutputStream(
             DataOutputStream output,
             TypedValueWriter valueWriter,
@@ -131,10 +138,10 @@ class UosObjectOutputStream extends ObjectOutputStream {
     /** {@inheritDoc} */
     @Override
     protected void writeObjectOverride(Object obj) throws IOException {
-        writeObject0(obj);
+        doWriteObject(obj);
     }
 
-    private void writeObject0(Object obj) throws IOException {
+    private void doWriteObject(Object obj) throws IOException {
         try {
             valueWriter.write(obj, objectClass(obj), output, context);
         } catch (MarshalException e) {
@@ -146,7 +153,7 @@ class UosObjectOutputStream extends ObjectOutputStream {
     @Override
     public void writeUnshared(Object obj) throws IOException {
         // TODO: IGNITE-16257 - implement 'unshared' logic?
-        writeObject0(obj);
+        doWriteObject(obj);
     }
 
     /** {@inheritDoc} */
@@ -166,6 +173,24 @@ class UosObjectOutputStream extends ObjectOutputStream {
 
     /** {@inheritDoc} */
     @Override
+    public PutField putFields() {
+        if (currentPut == null) {
+            currentPut = new UosPutField(context.descriptorOfObjectCurrentlyWrittenWithWriteObject());
+        }
+        return currentPut;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void writeFields() throws IOException {
+        if (currentPut == null) {
+            throw new NotActiveException("no current PutField object");
+        }
+        currentPut.write(this);
+    }
+
+    /** {@inheritDoc} */
+    @Override
     public void useProtocolVersion(int version) {
         // no op
     }
@@ -188,4 +213,111 @@ class UosObjectOutputStream extends ObjectOutputStream {
     public void close() throws IOException {
         flush();
     }
+
+    UosPutField replaceCurrentPutFieldWithNull() {
+        UosPutField oldPut = currentPut;
+        currentPut = null;
+        return oldPut;
+    }
+
+    void restoreCurrentPutFieldTo(UosPutField newPut) {
+        currentPut = newPut;
+    }
+
+    class UosPutField extends PutField {
+        private final ClassDescriptor descriptor;
+
+        private final byte[] primitiveFieldsData;
+        private final Object[] objectFieldVals;
+
+        private UosPutField(ClassDescriptor currentObjectDescriptor) {
+            this.descriptor = currentObjectDescriptor;
+
+            primitiveFieldsData = new byte[currentObjectDescriptor.primitiveFieldsDataSize()];
+            objectFieldVals = new Object[currentObjectDescriptor.objectFieldsCount()];
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void put(String name, boolean val) {
+            Bits.putBoolean(primitiveFieldsData, primitiveFieldDataOffset(name, boolean.class), val);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void put(String name, byte val) {
+            primitiveFieldsData[primitiveFieldDataOffset(name, byte.class)] = val;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void put(String name, char val) {
+            Bits.putChar(primitiveFieldsData, primitiveFieldDataOffset(name, char.class), val);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void put(String name, short val) {
+            Bits.putShort(primitiveFieldsData, primitiveFieldDataOffset(name, short.class), val);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void put(String name, int val) {
+            Bits.putInt(primitiveFieldsData, primitiveFieldDataOffset(name, int.class), val);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void put(String name, long val) {
+            Bits.putLong(primitiveFieldsData, primitiveFieldDataOffset(name, long.class), val);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void put(String name, float val) {
+            Bits.putFloat(primitiveFieldsData, primitiveFieldDataOffset(name, float.class), val);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void put(String name, double val) {
+            Bits.putDouble(primitiveFieldsData, primitiveFieldDataOffset(name, double.class), val);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void put(String name, Object val) {
+            objectFieldVals[objectFieldIndex(name)] = val;
+        }
+
+        private int primitiveFieldDataOffset(String fieldName, Class<?> requiredType) {
+            return descriptor.primitiveFieldDataOffset(fieldName, requiredType);
+        }
+
+        private int objectFieldIndex(String fieldName) {
+            return descriptor.objectFieldIndex(fieldName);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void write(ObjectOutput out) throws IOException {
+            if (out != UosObjectOutputStream.this) {
+                throw new IllegalArgumentException("This is not my output: " + out);
+            }
+
+            int objectFieldIndex = 0;
+
+            for (FieldDescriptor fieldDesc : descriptor.fields()) {
+                if (fieldDesc.isPrimitive()) {
+                    int offset = primitiveFieldDataOffset(fieldDesc.name(), fieldDesc.clazz());
+                    int length = Primitives.widthInBytes(fieldDesc.clazz());
+                    out.write(primitiveFieldsData, offset, length);
+                } else {
+                    doWriteObject(objectFieldVals[objectFieldIndex]);
+                    objectFieldIndex++;
+                }
+            }
+        }
+    }
 }
diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/PrimitivesTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/PrimitivesTest.java
new file mode 100644
index 0000000..7b99446
--- /dev/null
+++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/PrimitivesTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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.ignite.internal.network.serialization;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * Tests for {@link Primitives}.
+ */
+class PrimitivesTest {
+    @ParameterizedTest
+    @MethodSource("primitiveByteWidths")
+    void widthInBytesIsCorrectForPrimitiveTypes(Class<?> type, int expectedWidth) {
+        assertThat(Primitives.widthInBytes(type), is(expectedWidth));
+    }
+
+    private static Stream<Arguments> primitiveByteWidths() {
+        return Stream.of(
+                Arguments.of(byte.class, 1),
+                Arguments.of(short.class, 2),
+                Arguments.of(int.class, 4),
+                Arguments.of(long.class, 8),
+                Arguments.of(float.class, 4),
+                Arguments.of(double.class, 8),
+                Arguments.of(char.class, 2),
+                Arguments.of(boolean.class, 1)
+        );
+    }
+
+    @SuppressWarnings("ResultOfMethodCallIgnored")
+    @Test
+    void widthInBytesThrowsForNonPrimitiveTypes() {
+        assertThrows(IllegalArgumentException.class, () -> Primitives.widthInBytes(Object.class));
+    }
+}
diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest.java
index dc17421..0dd5706 100644
--- a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest.java
+++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest.java
@@ -17,11 +17,15 @@
 
 package org.apache.ignite.internal.network.serialization.marshal;
 
+import static org.apache.ignite.internal.network.serialization.marshal.Throwables.causalChain;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasItem;
+import static org.hamcrest.Matchers.hasProperty;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.lessThan;
 import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.nullValue;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -57,7 +61,7 @@ class DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest {
 
     private final DefaultUserObjectMarshaller marshaller = new DefaultUserObjectMarshaller(descriptorRegistry, descriptorFactory);
 
-    /** This is static so that writeObject()/readObject() can easily find it. */
+    /** Reader+writer is static so that writeObject()/readObject() can easily find it. */
     private static ReaderAndWriter<?> readerAndWriter;
 
     /** Static access to the marshaller (for using in parameterized tests). */
@@ -65,6 +69,11 @@ class DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest {
     /** Static access to the registry (for using in parameterized tests). */
     private static ClassDescriptorRegistry staticDescriptorRegistry;
 
+    /** Putter is static so that writeObject() can easily find it. */
+    private static FieldPutter fieldPutter;
+    /** Filler is static so that readObject() can easily find it. */
+    private static FieldFiller fieldFiller;
+
     @BeforeEach
     void initStatics() {
         staticMarshaller = marshaller;
@@ -203,9 +212,6 @@ class DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest {
         ).map(Arguments::of);
     }
 
-    // TODO: IGNITE-16240 - implement putFields()/writeFields()
-    // TODO: IGNITE-16240 - implement readFields()
-
     @SuppressWarnings("SameParameterValue")
     private static byte[] readBytes(InputStream is, int count) throws IOException {
         byte[] bytes = new byte[count];
@@ -254,6 +260,155 @@ class DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest {
     }
 
     @Test
+    void putFieldsWritesAllPrimitiveTypesAndObjectsSoThatDefaultUnmarshallingReadsThem() throws Exception {
+        fieldPutter = putField -> {
+            putField.put("byteVal", (byte) 101);
+            putField.put("shortVal", (short) 102);
+            putField.put("intVal", 103);
+            putField.put("longVal", (long) 104);
+            putField.put("floatVal", (float) 105);
+            putField.put("doubleVal", (double) 106);
+            putField.put("charVal", 'z');
+            putField.put("booleanVal", true);
+            putField.put("objectVal", new IntHolder(142));
+        };
+
+        WithPutFields unmarshalled = marshalAndUnmarshalNonNull(new WithPutFields());
+
+        assertThat(unmarshalled.byteVal, is((byte) 101));
+        assertThat(unmarshalled.shortVal, is((short) 102));
+        assertThat(unmarshalled.intVal, is(103));
+        assertThat(unmarshalled.longVal, is((long) 104));
+        assertThat(unmarshalled.floatVal, is((float) 105));
+        assertThat(unmarshalled.doubleVal, is((double) 106));
+        assertThat(unmarshalled.charVal, is('z'));
+        assertThat(unmarshalled.booleanVal, is(true));
+        assertThat(unmarshalled.objectVal, is(new IntHolder(142)));
+    }
+
+    @Test
+    void putFieldsWritesDefaultValuesForFieldsNotSpecifiedExplicitly() throws Exception {
+        fieldPutter = putField -> {
+            // do not put anything -> defaults should be written
+        };
+
+        WithPutFields unmarshalled = marshalAndUnmarshalNonNull(new WithPutFields());
+
+        assertThat(unmarshalled.byteVal, is((byte) 0));
+        assertThat(unmarshalled.shortVal, is((short) 0));
+        assertThat(unmarshalled.intVal, is(0));
+        assertThat(unmarshalled.longVal, is((long) 0));
+        assertThat(unmarshalled.floatVal, is((float) 0));
+        assertThat(unmarshalled.doubleVal, is((double) 0));
+        assertThat(unmarshalled.charVal, is('\0'));
+        assertThat(unmarshalled.booleanVal, is(false));
+        assertThat(unmarshalled.objectVal, is(nullValue()));
+    }
+
+    @Test
+    void putFieldsThrowsForAnUnknownFieldAccess() {
+        fieldPutter = putField -> putField.put("no-such-field", 1);
+
+        Exception exception = assertThrows(Exception.class, () -> marshalAndUnmarshalNonNull(new WithPutFields()));
+        assertThat(causalChain(exception), hasItem(hasProperty("message", equalTo("Did not find a field with name no-such-field"))));
+    }
+
+    @Test
+    void readFieldsReadsAllPrimitiveTypesAndObjectsWrittenWithDefaultMarshalling() throws Exception {
+        WithReadFields original = new WithReadFields();
+        original.byteVal = (byte) 101;
+        original.shortVal = (short) 102;
+        original.intVal = 103;
+        original.longVal = (long) 104;
+        original.floatVal = (float) 105;
+        original.doubleVal = (double) 106;
+        original.charVal = 'z';
+        original.booleanVal = true;
+        original.objectVal = new IntHolder(142);
+
+        fieldFiller = (getField, target) -> {
+            target.byteVal = getField.get("byteVal", (byte) 201);
+            target.shortVal = getField.get("shortVal", (short) 202);
+            target.intVal = getField.get("intVal", 203);
+            target.longVal = getField.get("longVal", (long) 204);
+            target.floatVal = getField.get("floatVal", (float) 205);
+            target.doubleVal = getField.get("doubleVal", (double) 206);
+            target.charVal = getField.get("charVal", '!');
+            target.booleanVal = getField.get("booleanVal", false);
+            target.objectVal = getField.get("objectVal", new IntHolder(242));
+        };
+
+        WithReadFields unmarshalled = marshalAndUnmarshalNonNull(original);
+
+        assertThat(unmarshalled.byteVal, is((byte) 101));
+        assertThat(unmarshalled.shortVal, is((short) 102));
+        assertThat(unmarshalled.intVal, is(103));
+        assertThat(unmarshalled.longVal, is((long) 104));
+        assertThat(unmarshalled.floatVal, is((float) 105));
+        assertThat(unmarshalled.doubleVal, is((double) 106));
+        assertThat(unmarshalled.charVal, is('z'));
+        assertThat(unmarshalled.booleanVal, is(true));
+        assertThat(unmarshalled.objectVal, is(new IntHolder(142)));
+    }
+
+    @Test
+    void whenReadFieldsIsUsedTheDefaultMechanismFillsNoFieldsItself() throws Exception {
+        fieldFiller = (getField, target) -> {
+            // do not fill anything, so default values must remain
+        };
+
+        WithReadFields unmarshalled = marshalAndUnmarshalNonNull(new WithReadFields());
+
+        assertThat(unmarshalled.byteVal, is((byte) 0));
+        assertThat(unmarshalled.shortVal, is((short) 0));
+        assertThat(unmarshalled.intVal, is(0));
+        assertThat(unmarshalled.longVal, is((long) 0));
+        assertThat(unmarshalled.floatVal, is((float) 0));
+        assertThat(unmarshalled.doubleVal, is((double) 0));
+        assertThat(unmarshalled.charVal, is('\0'));
+        assertThat(unmarshalled.booleanVal, is(false));
+        assertThat(unmarshalled.objectVal, is(nullValue()));
+    }
+
+    @Test
+    void getFieldsThrowsForAnUnknownFieldAccess() {
+        fieldFiller = (getField, target) -> getField.get("no-such-field", 1);
+
+        Exception exception = assertThrows(Exception.class, () -> marshalAndUnmarshalNonNull(new WithReadFields()));
+        assertThat(causalChain(exception), hasItem(hasProperty("message", equalTo("Did not find a field with name no-such-field"))));
+    }
+
+    @Test
+    void getFieldAlwaysReturnsFalseForDefaulted() {
+        // TODO: IGNITE-15948 - test that defaulted() works as intended when it's ready
+
+        fieldFiller = (getField, target) -> {
+            assertFalse(getField.defaulted("byteVal"));
+        };
+
+        assertDoesNotThrow(() -> marshalAndUnmarshalNonNull(new WithReadFields()));
+    }
+
+    @Test
+    void nestedPutReadFieldsAreSupported() throws Exception {
+        NestHostWithPutGetFields unmarshalled = marshalAndUnmarshalNonNull(new NestHostWithPutGetFields());
+
+        assertThat(unmarshalled.intValue, is(2));
+        assertThat(unmarshalled.objectValue, is(12));
+        assertThat(unmarshalled.nested.intValue, is(1));
+        assertThat(unmarshalled.nested.objectValue, is(11));
+    }
+
+    @Test
+    void readFieldsObjectStreamClassIsAccessible() {
+        fieldFiller = (getField, target) -> {
+            assertThat(getField.getObjectStreamClass().getName(), is(equalTo(WithReadFields.class.getName())));
+        };
+
+        assertDoesNotThrow(() -> marshalAndUnmarshalNonNull(new WithReadFields()));
+    }
+
+    @Test
     void supportsFlushInsideWriteObject() {
         readerAndWriter = new ReaderAndWriter<>(ObjectOutputStream::flush, ois -> null);
 
@@ -265,7 +420,7 @@ class DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest {
     }
 
     @Test
-    void resetThrowsInsideWriteObject() {
+    void resetErrorsOutInsideWriteObject() {
         readerAndWriter = new ReaderAndWriter<>(ObjectOutputStream::reset, ois -> null);
 
         assertThrows(MarshalException.class, this::marshalAndUnmarshalWithCustomizableOverride);
@@ -439,6 +594,103 @@ class DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest {
         }
     }
 
+    private interface FieldPutter {
+        void putWith(ObjectOutputStream.PutField putField);
+    }
+
+    @SuppressWarnings("FieldMayBeFinal")
+    private static class WithPutFields implements Serializable {
+        private byte byteVal = 1;
+        private short shortVal = 2;
+        private int intVal = 3;
+        private long longVal = 4;
+        private float floatVal = 5;
+        private double doubleVal = 6;
+        private char charVal = 'a';
+        private boolean booleanVal = true;
+        private Object objectVal = new IntHolder(42);
+
+        private void writeObject(ObjectOutputStream oos) throws IOException {
+            ObjectOutputStream.PutField putField = oos.putFields();
+            fieldPutter.putWith(putField);
+            oos.writeFields();
+        }
+
+        private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
+            ois.defaultReadObject();
+        }
+    }
+
+    private interface FieldFiller {
+        void fillWith(ObjectInputStream.GetField getField, WithReadFields target) throws IOException;
+    }
+
+    private static class WithReadFields implements Serializable {
+        private byte byteVal = 1;
+        private short shortVal = 2;
+        private int intVal = 3;
+        private long longVal = 4;
+        private float floatVal = 5;
+        private double doubleVal = 6;
+        private char charVal = 'a';
+        private boolean booleanVal = true;
+        private Object objectVal = new IntHolder(42);
+
+        private void writeObject(ObjectOutputStream oos) throws IOException {
+            oos.defaultWriteObject();
+        }
+
+        private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
+            ObjectInputStream.GetField getField = ois.readFields();
+            fieldFiller.fillWith(getField, this);
+        }
+    }
+
+    private static class NestedWithPutFields implements Serializable {
+        private int intValue = 1;
+        private Object objectValue = 11;
+
+        private void writeObject(ObjectOutputStream stream) throws IOException {
+            ObjectOutputStream.PutField putField = stream.putFields();
+
+            putField.put("intValue", intValue);
+            putField.put("objectValue", objectValue);
+
+            stream.writeFields();
+        }
+
+        private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+            ObjectInputStream.GetField getField = stream.readFields();
+
+            intValue = getField.get("intValue", -1);
+            objectValue = getField.get("objectValue", null);
+        }
+    }
+
+    private static class NestHostWithPutGetFields implements Serializable {
+        private int intValue = 2;
+        private Object objectValue = 12;
+        private NestedWithPutFields nested = new NestedWithPutFields();
+
+        private void writeObject(ObjectOutputStream stream) throws IOException {
+            ObjectOutputStream.PutField putField = stream.putFields();
+
+            putField.put("intValue", intValue);
+            putField.put("objectValue", objectValue);
+            putField.put("nested", nested);
+
+            stream.writeFields();
+        }
+
+        private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
+            ObjectInputStream.GetField getField = stream.readFields();
+
+            intValue = getField.get("intValue", -1);
+            objectValue = getField.get("objectValue", null);
+            nested = (NestedWithPutFields) getField.get("nested", null);
+        }
+    }
+
     private static class SimpleNonSerializable {
         private int value;
 
diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/IntHolder.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/IntHolder.java
index 2a07715..dc10e31 100644
--- a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/IntHolder.java
+++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/IntHolder.java
@@ -17,6 +17,8 @@
 
 package org.apache.ignite.internal.network.serialization.marshal;
 
+import java.util.Objects;
+
 /**
  * Holds an int value.
  */
@@ -26,4 +28,31 @@ class IntHolder {
     IntHolder(int value) {
         this.value = value;
     }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        IntHolder intHolder = (IntHolder) o;
+        return value == intHolder.value;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(value);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "IntHolder{"
+                + "value=" + value
+                + '}';
+    }
 }