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/17 08:16:58 UTC

[ignite-3] branch main updated: IGNITE-16254 - Optimize (un)marshalling of primitives in object fields

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 40841cd  IGNITE-16254 - Optimize (un)marshalling of primitives in object fields
40841cd is described below

commit 40841cd94a44b45b00f963e0588e79cee9f135a1
Author: Roman Puchkovskiy <ro...@gmail.com>
AuthorDate: Thu Jan 13 14:29:22 2022 +0400

    IGNITE-16254 - Optimize (un)marshalling of primitives in object fields
---
 .../network/message/ClassDescriptorMessage.java    |  13 +-
 .../{BuiltinType.java => BuiltInType.java}         |  27 +--
 ...izedStreamCommands.java => BuiltInTypeIds.java} |  21 +--
 .../network/serialization/ClassDescriptor.java     |  30 +++-
 .../serialization/ClassDescriptorFactory.java      |  43 +++--
 .../ClassDescriptorFactoryContext.java             |  33 +---
 .../network/serialization/FieldAccessor.java       | 136 ++++++++++++++-
 .../network/serialization/FieldAccessorImpl.java   |  83 ---------
 .../network/serialization/FieldDescriptor.java     |  24 +--
 .../serialization/IdIndexedDescriptors.java        |   9 +
 .../PerSessionSerializationService.java            |   6 +-
 .../network/serialization/Serialization.java       |  38 ++--
 .../network/serialization/SerializationType.java   |   2 +-
 .../SpecialSerializationMethodsImpl.java           |   4 +-
 .../network/serialization/UnsafeFieldAccessor.java | 192 +++++++++++++++++++++
 .../marshal/DefaultUserObjectMarshaller.java       |  11 +-
 .../marshal/StructuredObjectMarshaller.java        |  98 ++++++++++-
 .../serialization/ClassDescriptorFactoryTest.java  |  60 ++++++-
 .../serialization/DefaultDescriptorsTest.java      |  90 +++++-----
 ...erObjectMarshallerWithArbitraryObjectsTest.java |  78 ++++++++-
 ...efaultUserObjectMarshallerWithBuiltinsTest.java | 112 ++++++------
 ...shallerWithSerializableOverrideStreamsTest.java |   2 -
 ...ltUserObjectMarshallerWithSerializableTest.java |  28 +++
 23 files changed, 817 insertions(+), 323 deletions(-)

diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/message/ClassDescriptorMessage.java b/modules/network/src/main/java/org/apache/ignite/internal/network/message/ClassDescriptorMessage.java
index 1bf857b..c79d5c6 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/message/ClassDescriptorMessage.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/message/ClassDescriptorMessage.java
@@ -73,11 +73,18 @@ public interface ClassDescriptorMessage extends NetworkMessage {
     int serializationType();
 
     /**
-     * Has serialization override.
+     * Has writeObject().
      *
-     * @see Serialization#hasSerializationOverride()
+     * @see Serialization#hasWriteObject()
      */
-    boolean hasSerializationOverride();
+    boolean hasWriteObject();
+
+    /**
+     * Has readObject().
+     *
+     * @see Serialization#hasReadObject()
+     */
+    boolean hasReadObject();
 
     /**
      * Has readObjectNoData().
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltinType.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltInType.java
similarity index 87%
rename from modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltinType.java
rename to modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltInType.java
index c27c595..56f6f2b 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltinType.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltInType.java
@@ -31,23 +31,21 @@ import org.apache.ignite.lang.IgniteUuid;
 
 /**
  * Built-in types.
- *
- * <p>They share ID space with commands defined in {@link SerializedStreamCommands}.
  */
-public enum BuiltinType {
-    BYTE(0, byte.class),
+public enum BuiltInType {
+    BYTE(BuiltInTypeIds.BYTE, byte.class),
     BYTE_BOXED(1, Byte.class),
-    SHORT(2, short.class),
+    SHORT(BuiltInTypeIds.SHORT, short.class),
     SHORT_BOXED(3, Short.class),
-    INT(4, int.class),
+    INT(BuiltInTypeIds.INT, int.class),
     INT_BOXED(5, Integer.class),
-    FLOAT(6, float.class),
+    FLOAT(BuiltInTypeIds.FLOAT, float.class),
     FLOAT_BOXED(7, Float.class),
-    LONG(8, long.class),
+    LONG(BuiltInTypeIds.LONG, long.class),
     LONG_BOXED(9, Long.class),
-    DOUBLE(10, double.class),
+    DOUBLE(BuiltInTypeIds.DOUBLE, double.class),
     DOUBLE_BOXED(11, Double.class),
-    BOOLEAN(12, boolean.class),
+    BOOLEAN(BuiltInTypeIds.BOOLEAN, boolean.class),
     BOOLEAN_BOXED(13, Boolean.class),
     CHAR(14, char.class),
     CHAR_BOXED(15, Character.class),
@@ -80,8 +78,8 @@ public enum BuiltinType {
     HASH_MAP(40, HashMap.class),
     LINKED_HASH_MAP(41, LinkedHashMap.class),
     BIT_SET(42, BitSet.class),
-    NULL(43, Null.class)
-    // 44 is REFERENCE command, see SerializedStreamCommands#REFERENCE
+    NULL(43, Null.class),
+    REFERENCE(44, DummyPlaceholder.class)
     ;
 
     /**
@@ -100,7 +98,7 @@ public enum BuiltinType {
      * @param descriptorId Descriptor id.
      * @param clazz        Type.
      */
-    BuiltinType(int descriptorId, Class<?> clazz) {
+    BuiltInType(int descriptorId, Class<?> clazz) {
         this.descriptorId = descriptorId;
         this.clazz = clazz;
     }
@@ -137,4 +135,7 @@ public enum BuiltinType {
                 new Serialization(SerializationType.BUILTIN)
         );
     }
+
+    private static class DummyPlaceholder {
+    }
 }
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializedStreamCommands.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltInTypeIds.java
similarity index 61%
rename from modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializedStreamCommands.java
rename to modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltInTypeIds.java
index b5f1af3..fe1380b 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializedStreamCommands.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltInTypeIds.java
@@ -18,15 +18,16 @@
 package org.apache.ignite.internal.network.serialization;
 
 /**
- * Lists commands used in the serialized stream.
- * Command IDs share space with IDs of {@link ClassDescriptor}s (most importantly, those that are defined in {@link BuiltinType}.
+ * IDs of built-in descriptors. Only defines part of IDs which are needed directly in constant expressions (like switch);
+ * most of them are defined directly on enum members in {@link BuiltInType}.
  */
-public class SerializedStreamCommands {
-    /**
-     * Reference: an object that was already seen in the graph, so we relate to it by its ID instead of serializing it again.
-     */
-    public static final int REFERENCE = 44;
-
-    private SerializedStreamCommands() {
-    }
+public class BuiltInTypeIds {
+    public static final int BYTE = 0;
+    public static final int SHORT = 2;
+    public static final int INT = 4;
+    public static final int FLOAT = 6;
+    public static final int LONG = 8;
+    public static final int DOUBLE = 10;
+    public static final int BOOLEAN = 12;
+    public static final int CHAR = 14;
 }
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 322ffcf..247bf09 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
@@ -206,12 +206,30 @@ public class ClassDescriptor {
     }
 
     /**
-     * Returns {@code true} if the described class has writeObject() and readObject() methods.
+     * Returns {@code true} if the described class has writeObject() method.
      *
-     * @return {@code true} if the described class has writeObject() and readObject() methods
+     * @return {@code true} if the described class has writeObject() method
      */
-    public boolean hasSerializationOverride() {
-        return serialization.hasSerializationOverride();
+    public boolean hasWriteObject() {
+        return serialization.hasWriteObject();
+    }
+
+    /**
+     * Returns {@code true} if the described class has readObject() method.
+     *
+     * @return {@code true} if the described class has readObject() method
+     */
+    public boolean hasReadObject() {
+        return serialization.hasReadObject();
+    }
+
+    /**
+     * Returns {@code true} if the described class has readObjectNoData() method.
+     *
+     * @return {@code true} if the described class has readObjectNoData() method
+     */
+    public boolean hasReadObjectNoData() {
+        return serialization.hasReadObjectNoData();
     }
 
     /**
@@ -238,7 +256,7 @@ public class ClassDescriptor {
      * @return {@code true} if this is the descriptor of {@code null} values
      */
     public boolean isNull() {
-        return descriptorId == BuiltinType.NULL.descriptorId();
+        return descriptorId == BuiltInType.NULL.descriptorId();
     }
 
     /**
@@ -247,7 +265,7 @@ public class ClassDescriptor {
      * @return {@code true} if this is the descriptor of {@link java.util.Collections#singletonList(Object)} type
      */
     public boolean isSingletonList() {
-        return descriptorId == BuiltinType.SINGLETON_LIST.descriptorId();
+        return descriptorId == BuiltInType.SINGLETON_LIST.descriptorId();
     }
 
     /**
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactory.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactory.java
index 966a7ff..600ec6e 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactory.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactory.java
@@ -40,8 +40,10 @@ import org.jetbrains.annotations.Nullable;
  * Class descriptor factory for the user object serialization.
  */
 public class ClassDescriptorFactory {
-    /** Means that no serialization override is present; used for readability instead of {@code false}. */
-    private static final boolean NO_SERIALIZATION_OVERRIDE = false;
+    /** Means that no writeObject() method is present; used for readability instead of {@code false}. */
+    private static final boolean NO_WRITE_OBJECT = false;
+    /** Means that no readObject() method is present; used for readability instead of {@code false}. */
+    private static final boolean NO_READ_OBJECT = false;
     /** Means that no readObjectNoData() method is present; used for readability instead of {@code false}. */
     private static final boolean NO_READ_OBJECT_NO_DATA = false;
 
@@ -133,7 +135,8 @@ public class ClassDescriptorFactory {
                 Collections.emptyList(),
                 new Serialization(
                         SerializationType.EXTERNALIZABLE,
-                        NO_SERIALIZATION_OVERRIDE,
+                        NO_WRITE_OBJECT,
+                        NO_READ_OBJECT,
                         NO_READ_OBJECT_NO_DATA,
                         hasWriteReplace(clazz),
                         hasReadResolve(clazz)
@@ -201,7 +204,8 @@ public class ClassDescriptorFactory {
                 fields(clazz),
                 new Serialization(
                         SerializationType.SERIALIZABLE,
-                        hasOverrideSerialization(clazz),
+                        hasWriteObject(clazz),
+                        hasReadObject(clazz),
                         hasReadObjectNoData(clazz),
                         hasWriteReplace(clazz),
                         hasReadResolve(clazz)
@@ -217,10 +221,6 @@ public class ClassDescriptorFactory {
         return getWriteReplace(clazz) != null;
     }
 
-    private boolean hasOverrideSerialization(Class<? extends Serializable> clazz) {
-        return hasWriteObject(clazz) && hasReadObject(clazz);
-    }
-
     private boolean hasReadObject(Class<? extends Serializable> clazz) {
         return getReadObject(clazz) != null;
     }
@@ -312,13 +312,16 @@ public class ClassDescriptorFactory {
     @Nullable
     private static Method getWriteObject(Class<? extends Serializable> clazz) {
         try {
-            Method writeObject = clazz.getDeclaredMethod("writeObject", ObjectOutputStream.class);
+            Method method = clazz.getDeclaredMethod("writeObject", ObjectOutputStream.class);
 
-            if (!Modifier.isPrivate(writeObject.getModifiers())) {
+            if (!Modifier.isPrivate(method.getModifiers())) {
+                return null;
+            }
+            if (method.getReturnType() != void.class) {
                 return null;
             }
 
-            return writeObject;
+            return method;
         } catch (NoSuchMethodException e) {
             return null;
         }
@@ -335,13 +338,16 @@ public class ClassDescriptorFactory {
     @Nullable
     private static Method getReadObject(Class<? extends Serializable> clazz) {
         try {
-            Method writeObject = clazz.getDeclaredMethod("readObject", ObjectInputStream.class);
+            Method method = clazz.getDeclaredMethod("readObject", ObjectInputStream.class);
 
-            if (!Modifier.isPrivate(writeObject.getModifiers())) {
+            if (!Modifier.isPrivate(method.getModifiers())) {
+                return null;
+            }
+            if (method.getReturnType() != void.class) {
                 return null;
             }
 
-            return writeObject;
+            return method;
         } catch (NoSuchMethodException e) {
             return null;
         }
@@ -357,13 +363,16 @@ public class ClassDescriptorFactory {
     @Nullable
     private static Method getReadObjectNoData(Class<? extends Serializable> clazz) {
         try {
-            Method writeObject = clazz.getDeclaredMethod("readObjectNoData");
+            Method method = clazz.getDeclaredMethod("readObjectNoData");
 
-            if (!Modifier.isPrivate(writeObject.getModifiers())) {
+            if (!Modifier.isPrivate(method.getModifiers())) {
+                return null;
+            }
+            if (method.getReturnType() != void.class) {
                 return null;
             }
 
-            return writeObject;
+            return method;
         } catch (NoSuchMethodException e) {
             return null;
         }
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactoryContext.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactoryContext.java
index 47cba73..473404c 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactoryContext.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactoryContext.java
@@ -25,7 +25,7 @@ import org.jetbrains.annotations.Nullable;
 /**
  * Class descriptor factory context.
  */
-public class ClassDescriptorFactoryContext implements ClassIndexedDescriptors {
+public class ClassDescriptorFactoryContext implements IdIndexedDescriptors, ClassIndexedDescriptors {
     /** Quantity of descriptor ids reserved for the default descriptors. */
     private static final int DEFAULT_DESCRIPTORS_OFFSET_COUNT = 1000;
 
@@ -42,7 +42,7 @@ public class ClassDescriptorFactoryContext implements ClassIndexedDescriptors {
      * Constructor.
      */
     public ClassDescriptorFactoryContext() {
-        for (BuiltinType value : BuiltinType.values()) {
+        for (BuiltInType value : BuiltInType.values()) {
             addPredefinedDescriptor(value.clazz(), value.asClassDescriptor());
         }
     }
@@ -81,6 +81,7 @@ public class ClassDescriptorFactoryContext implements ClassIndexedDescriptors {
      * @param descriptorId Descriptor id.
      * @return Descriptor.
      */
+    @Override
     @Nullable
     public ClassDescriptor getDescriptor(int descriptorId) {
         return descriptorMap.get(descriptorId);
@@ -109,37 +110,11 @@ public class ClassDescriptorFactoryContext implements ClassIndexedDescriptors {
      *
      * @param builtinType   built-in type for lookup
      */
-    public ClassDescriptor getBuiltInDescriptor(BuiltinType builtinType) {
+    public ClassDescriptor getBuiltInDescriptor(BuiltInType builtinType) {
         return getRequiredDescriptor(builtinType.descriptorId());
     }
 
     /**
-     * Returns a descriptor by ID or throws an exception if no such descriptor is known.
-     *
-     * @param descriptorId ID of the descriptor
-     * @return descriptor by ID
-     */
-    public ClassDescriptor getRequiredDescriptor(int descriptorId) {
-        ClassDescriptor descriptor = getDescriptor(descriptorId);
-
-        if (descriptor == null) {
-            throw new IllegalStateException("Did not find a descriptor with ID=" + descriptorId);
-        }
-
-        return descriptor;
-    }
-
-    /**
-     * Returns {@code true} if there is a descriptor for the id.
-     *
-     * @param descriptorId Descriptor id.
-     * @return {@code true} if there is a descriptor for the id.
-     */
-    public boolean hasDescriptor(int descriptorId) {
-        return getDescriptor(descriptorId) != null;
-    }
-
-    /**
      * Returns a descriptor for {@code null} value.
      *
      * @return a descriptor for {@code null} value
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessor.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessor.java
index 34c4806..81b3b88 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessor.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessor.java
@@ -22,18 +22,146 @@ package org.apache.ignite.internal.network.serialization;
  */
 public interface FieldAccessor {
     /**
-     * Returns the bound field value of the given object.
+     * Returns the bound object field value of the given object.
      *
      * @param target target object
      * @return the bound field value of the given object
      */
-    Object get(Object target);
+    Object getObject(Object target);
 
     /**
-     * Sets the bound field value on the given object.
+     * Sets the bound object field value on the given object.
      *
      * @param target     target object
      * @param fieldValue value to set
      */
-    void set(Object target, Object fieldValue);
+    void setObject(Object target, Object fieldValue);
+
+    /**
+     * Returns the bound byte field value of the given object.
+     *
+     * @param target target object
+     * @return the bound byte field value of the given object
+     */
+    byte getByte(Object target);
+
+    /**
+     * Sets the bound byte field value on the given object.
+     *
+     * @param target     target object
+     * @param fieldValue value to set
+     */
+    void setByte(Object target, byte fieldValue);
+
+    /**
+     * Returns the bound short field value of the given object.
+     *
+     * @param target target object
+     * @return the bound byte field value of the given object
+     */
+    short getShort(Object target);
+
+    /**
+     * Sets the bound short field value on the given object.
+     *
+     * @param target     target object
+     * @param fieldValue value to set
+     */
+    void setShort(Object target, short fieldValue);
+
+    /**
+     * Returns the bound int field value of the given object.
+     *
+     * @param target target object
+     * @return the bound int field value of the given object
+     */
+    int getInt(Object target);
+
+    /**
+     * Sets the bound int field value on the given object.
+     *
+     * @param target     target object
+     * @param fieldValue value to set
+     */
+    void setInt(Object target, int fieldValue);
+
+    /**
+     * Returns the bound long field value of the given object.
+     *
+     * @param target target object
+     * @return the bound byte field value of the given object
+     */
+    long getLong(Object target);
+
+    /**
+     * Sets the bound long field value on the given object.
+     *
+     * @param target     target object
+     * @param fieldValue value to set
+     */
+    void setLong(Object target, long fieldValue);
+
+    /**
+     * Returns the bound float field value of the given object.
+     *
+     * @param target target object
+     * @return the bound byte field value of the given object
+     */
+    float getFloat(Object target);
+
+    /**
+     * Sets the bound float field value on the given object.
+     *
+     * @param target     target object
+     * @param fieldValue value to set
+     */
+    void setFloat(Object target, float fieldValue);
+
+    /**
+     * Returns the bound double field value of the given object.
+     *
+     * @param target target object
+     * @return the bound byte field value of the given object
+     */
+    double getDouble(Object target);
+
+    /**
+     * Sets the bound double field value on the given object.
+     *
+     * @param target     target object
+     * @param fieldValue value to set
+     */
+    void setDouble(Object target, double fieldValue);
+
+    /**
+     * Returns the bound char field value of the given object.
+     *
+     * @param target target object
+     * @return the bound byte field value of the given object
+     */
+    char getChar(Object target);
+
+    /**
+     * Sets the bound char field value on the given object.
+     *
+     * @param target     target object
+     * @param fieldValue value to set
+     */
+    void setChar(Object target, char fieldValue);
+
+    /**
+     * Returns the bound boolean field value of the given object.
+     *
+     * @param target target object
+     * @return the bound byte field value of the given object
+     */
+    boolean getBoolean(Object target);
+
+    /**
+     * Sets the bound boolean field value on the given object.
+     *
+     * @param target     target object
+     * @param fieldValue value to set
+     */
+    void setBoolean(Object target, boolean fieldValue);
 }
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessorImpl.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessorImpl.java
deleted file mode 100644
index 1346397..0000000
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/FieldAccessorImpl.java
+++ /dev/null
@@ -1,83 +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.ignite.internal.network.serialization;
-
-import java.lang.invoke.MethodHandles;
-import java.lang.invoke.VarHandle;
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-
-/**
- * {@link FieldAccessor} implementation.
- */
-class FieldAccessorImpl implements FieldAccessor {
-    private final Field field;
-    private final VarHandle varHandle;
-
-    FieldAccessorImpl(FieldDescriptor descriptor) {
-        field = findField(descriptor);
-        field.setAccessible(true);
-
-        varHandle = varHandleFrom(field);
-    }
-
-    private static Field findField(FieldDescriptor fieldDescriptor) {
-        try {
-            return fieldDescriptor.declaringClass().getDeclaredField(fieldDescriptor.name());
-        } catch (NoSuchFieldException e) {
-            throw new ReflectionException("Cannot find field", e);
-        }
-    }
-
-    private static VarHandle varHandleFrom(Field field) {
-        try {
-            MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(field.getDeclaringClass(), MethodHandles.lookup());
-            return lookup.unreflectVarHandle(field);
-        } catch (ReflectiveOperationException e) {
-            throw new ReflectionException("Cannot get a field VarHandle", e);
-        }
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public Object get(Object target) {
-        return varHandle.get(target);
-    }
-
-    /** {@inheritDoc} */
-    @Override
-    public void set(Object target, Object fieldValue) {
-        if (isFieldFinal()) {
-            setViaField(target, fieldValue);
-        } else {
-            varHandle.set(target, fieldValue);
-        }
-    }
-
-    private boolean isFieldFinal() {
-        return Modifier.isFinal(field.getModifiers());
-    }
-
-    private void setViaField(Object target, Object fieldValue) {
-        try {
-            field.set(target, fieldValue);
-        } catch (IllegalAccessException e) {
-            throw new ReflectionException("Cannot set a value", e);
-        }
-    }
-}
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 fe86f24..e3e4687 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
@@ -40,11 +40,6 @@ public class FieldDescriptor {
     private final int typeDescriptorId;
 
     /**
-     * The class in which the field is declared.
-     */
-    private final Class<?> declaringClass;
-
-    /**
      * Accessor for accessing this field.
      */
     private final FieldAccessor accessor;
@@ -53,7 +48,7 @@ public class FieldDescriptor {
      * Constructor.
      */
     public FieldDescriptor(Field field, int typeDescriptorId) {
-        this(field.getName(), field.getType(), typeDescriptorId, field.getDeclaringClass());
+        this(field.getName(), field.getType(), typeDescriptorId, new UnsafeFieldAccessor(field));
     }
 
     /**
@@ -65,12 +60,14 @@ public class FieldDescriptor {
      * @param declaringClass    the class in which the field if declared
      */
     public FieldDescriptor(String fieldName, Class<?> fieldClazz, int typeDescriptorId, Class<?> declaringClass) {
+        this(fieldName, fieldClazz, typeDescriptorId, new UnsafeFieldAccessor(fieldName, declaringClass));
+    }
+
+    private FieldDescriptor(String fieldName, Class<?> fieldClazz, int typeDescriptorId, FieldAccessor accessor) {
         this.name = fieldName;
         this.clazz = fieldClazz;
         this.typeDescriptorId = typeDescriptorId;
-        this.declaringClass = declaringClass;
-
-        accessor = new FieldAccessorImpl(this);
+        this.accessor = accessor;
     }
 
     /**
@@ -103,15 +100,6 @@ public class FieldDescriptor {
     }
 
     /**
-     * Returns the class in which the field is declared.
-     *
-     * @return the class in which the field is declared
-     */
-    public Class<?> declaringClass() {
-        return declaringClass;
-    }
-
-    /**
      * 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/IdIndexedDescriptors.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/IdIndexedDescriptors.java
index b5938a8..0881bb6 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/IdIndexedDescriptors.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/IdIndexedDescriptors.java
@@ -48,4 +48,13 @@ public interface IdIndexedDescriptors {
         return descriptor;
     }
 
+    /**
+     * Returns {@code true} if there is a descriptor for the id.
+     *
+     * @param descriptorId Descriptor id.
+     * @return {@code true} if there is a descriptor for the id.
+     */
+    default boolean hasDescriptor(int descriptorId) {
+        return getDescriptor(descriptorId) != null;
+    }
 }
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/PerSessionSerializationService.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/PerSessionSerializationService.java
index fb92bd0..167649e 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/PerSessionSerializationService.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/PerSessionSerializationService.java
@@ -128,7 +128,8 @@ public class PerSessionSerializationService {
                     .fields(fields)
                     .isFinal(descriptor.isFinal())
                     .serializationType(serialization.type().value())
-                    .hasSerializationOverride(serialization.hasSerializationOverride())
+                    .hasWriteObject(serialization.hasWriteObject())
+                    .hasReadObject(serialization.hasReadObject())
                     .hasReadObjectNoData(serialization.hasReadObjectNoData())
                     .hasWriteReplace(serialization.hasWriteReplace())
                     .hasReadResolve(serialization.hasReadResolve())
@@ -187,7 +188,8 @@ public class PerSessionSerializationService {
 
         var serialization = new Serialization(
                 serializationType,
-                clsMsg.hasSerializationOverride(),
+                clsMsg.hasWriteObject(),
+                clsMsg.hasReadObject(),
                 clsMsg.hasReadObjectNoData(),
                 clsMsg.hasWriteReplace(),
                 clsMsg.hasReadResolve()
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/Serialization.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/Serialization.java
index 19e247a..feff989 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/Serialization.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/Serialization.java
@@ -24,8 +24,10 @@ public class Serialization {
     /** Serialization type. */
     private final SerializationType type;
 
-    /** Whether a Serializable has writeObject() + readObject() methods. */
-    private final boolean hasSerializationOverride;
+    /** Whether a Serializable has writeObject() method. */
+    private final boolean hasWriteObject;
+    /** Whether a Serializable has readObject() method. */
+    private final boolean hasReadObject;
     /** Whether a Serializable has readObjectNoData() method. */
     private final boolean hasReadObjectNoData;
     /** Whether a Serializable/Externalizable has writeReplace() method. */
@@ -37,24 +39,27 @@ public class Serialization {
      * Creates a new Serialization.
      *
      * @param type                     type
-     * @param hasSerializationOverride whether a Serializable has writeObject() + readObject() methods
+     * @param hasWriteObject           whether a Serializable has writeObject() method
+     * @param hasReadObject            whether a Serializable has readObject() method
      * @param hasReadObjectNoData      whether a Serializable has readObjectNoData() method
      * @param hasWriteReplace          whether a Serializable/Externalizable has writeReplace() method
      * @param hasReadResolve           whether a Serializable/Externalizable has readResolve() method
      */
     public Serialization(
             SerializationType type,
-            boolean hasSerializationOverride,
+            boolean hasWriteObject,
+            boolean hasReadObject,
             boolean hasReadObjectNoData,
             boolean hasWriteReplace,
             boolean hasReadResolve
     ) {
         assert type == SerializationType.SERIALIZABLE
-                || (type == SerializationType.EXTERNALIZABLE && !hasSerializationOverride && !hasReadObjectNoData)
-                || (!hasSerializationOverride && !hasWriteReplace && !hasReadResolve);
+                || (type == SerializationType.EXTERNALIZABLE && !hasWriteObject && !hasReadObject && !hasReadObjectNoData)
+                || (!hasWriteObject && !hasReadObject && !hasWriteReplace && !hasReadResolve);
 
         this.type = type;
-        this.hasSerializationOverride = hasSerializationOverride;
+        this.hasWriteObject = hasWriteObject;
+        this.hasReadObject = hasReadObject;
         this.hasReadObjectNoData = hasReadObjectNoData;
         this.hasWriteReplace = hasWriteReplace;
         this.hasReadResolve = hasReadResolve;
@@ -66,7 +71,7 @@ public class Serialization {
      * @param type serialization type
      */
     public Serialization(SerializationType type) {
-        this(type, false, false, false, false);
+        this(type, false, false, false, false, false);
     }
 
     /**
@@ -79,12 +84,21 @@ public class Serialization {
     }
 
     /**
-     * Returns whether serialization override (writeObject() + readObject()) is present.
+     * Returns whether writeObject() method is present.
      *
-     * @return whether serialization override (writeObject() + readObject()) is present
+     * @return whether writeObject() method is present
      */
-    public boolean hasSerializationOverride() {
-        return hasSerializationOverride;
+    public boolean hasWriteObject() {
+        return hasWriteObject;
+    }
+
+    /**
+     * Returns whether readObject() method is present.
+     *
+     * @return whether readObject() method is present
+     */
+    public boolean hasReadObject() {
+        return hasReadObject;
     }
 
     /**
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializationType.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializationType.java
index 8f8b3dd..4a55f0d 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializationType.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SerializationType.java
@@ -23,7 +23,7 @@ import org.apache.ignite.lang.IgniteInternalException;
  * Serialization type.
  */
 public enum SerializationType {
-    /** Used for predefined descriptors like primitive (or boxed int). See {@link BuiltinType}. */
+    /** Used for predefined descriptors like primitive (or boxed int). See {@link BuiltInType}. */
     BUILTIN(0),
     /** Type for classes that are neither serializable nor externalizable.  */
     ARBITRARY(1),
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SpecialSerializationMethodsImpl.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SpecialSerializationMethodsImpl.java
index def5ff4..84e7caa 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SpecialSerializationMethodsImpl.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/SpecialSerializationMethodsImpl.java
@@ -53,8 +53,8 @@ class SpecialSerializationMethodsImpl implements SpecialSerializationMethods {
     public SpecialSerializationMethodsImpl(ClassDescriptor descriptor) {
         writeReplaceHandle = descriptor.hasWriteReplace() ? writeReplaceHandle(descriptor) : null;
         readResolveHandle = descriptor.hasReadResolve() ? readResolveHandle(descriptor) : null;
-        writeObjectHandle = descriptor.hasSerializationOverride() ? writeObjectHandle(descriptor) : null;
-        readObjectHandle = descriptor.hasSerializationOverride() ? readObjectHandle(descriptor) : null;
+        writeObjectHandle = descriptor.hasWriteObject() ? writeObjectHandle(descriptor) : null;
+        readObjectHandle = descriptor.hasReadObject() ? readObjectHandle(descriptor) : null;
     }
 
     private static MethodHandle writeReplaceHandle(ClassDescriptor descriptor) {
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/UnsafeFieldAccessor.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/UnsafeFieldAccessor.java
new file mode 100644
index 0000000..f002fcf
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/UnsafeFieldAccessor.java
@@ -0,0 +1,192 @@
+/*
+ * 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 java.lang.reflect.Field;
+import org.apache.ignite.internal.util.GridUnsafe;
+
+/**
+ * {@link FieldAccessor} implementation.
+ */
+class UnsafeFieldAccessor implements FieldAccessor {
+    private final Field field;
+    private final Class<?> fieldType;
+    private final long fieldOffset;
+
+    UnsafeFieldAccessor(String fieldName, Class<?> declaringClass) {
+        this(findField(fieldName, declaringClass));
+    }
+
+    UnsafeFieldAccessor(Field field) {
+        this.field = field;
+        fieldType = field.getType();
+        fieldOffset = GridUnsafe.objectFieldOffset(field);
+    }
+
+    private static Field findField(String fieldName, Class<?> declaringClass) {
+        try {
+            return declaringClass.getDeclaredField(fieldName);
+        } catch (NoSuchFieldException e) {
+            throw new ReflectionException("Cannot find field", e);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Object getObject(Object target) {
+        assert !fieldType.isPrimitive() : field.getDeclaringClass() + "#" + field.getName() + " is primitive!";
+
+        return GridUnsafe.getObjectField(target, fieldOffset);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setObject(Object target, Object fieldValue) {
+        assert !fieldType.isPrimitive() : field.getDeclaringClass() + "#" + field.getName() + " is primitive!";
+
+        GridUnsafe.putObjectField(target, fieldOffset, fieldValue);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public byte getByte(Object target) {
+        assert fieldType == byte.class : field.getDeclaringClass() + "#" + field.getName() + " is not byte";
+
+        return GridUnsafe.getByteField(target, fieldOffset);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setByte(Object target, byte fieldValue) {
+        assert fieldType == byte.class : field.getDeclaringClass() + "#" + field.getName() + " is not byte";
+
+        GridUnsafe.putByteField(target, fieldOffset, fieldValue);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public short getShort(Object target) {
+        assert fieldType == short.class : field.getDeclaringClass() + "#" + field.getName() + " is not short";
+
+        return GridUnsafe.getShortField(target, fieldOffset);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setShort(Object target, short fieldValue) {
+        assert fieldType == short.class : field.getDeclaringClass() + "#" + field.getName() + " is not short";
+
+        GridUnsafe.putShortField(target, fieldOffset, fieldValue);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int getInt(Object target) {
+        assert fieldType == int.class : field.getDeclaringClass() + "#" + field.getName() + " is not int";
+
+        return GridUnsafe.getIntField(target, fieldOffset);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setInt(Object target, int fieldValue) {
+        assert fieldType == int.class : field.getDeclaringClass() + "#" + field.getName() + " is not int";
+
+        GridUnsafe.putIntField(target, fieldOffset, fieldValue);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public long getLong(Object target) {
+        assert fieldType == long.class : field.getDeclaringClass() + "#" + field.getName() + " is not long";
+
+        return GridUnsafe.getLongField(target, fieldOffset);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setLong(Object target, long fieldValue) {
+        assert fieldType == long.class : field.getDeclaringClass() + "#" + field.getName() + " is not long";
+
+        GridUnsafe.putLongField(target, fieldOffset, fieldValue);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public float getFloat(Object target) {
+        assert fieldType == float.class : field.getDeclaringClass() + "#" + field.getName() + " is not float";
+
+        return GridUnsafe.getFloatField(target, fieldOffset);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setFloat(Object target, float fieldValue) {
+        assert fieldType == float.class : field.getDeclaringClass() + "#" + field.getName() + " is not float";
+
+        GridUnsafe.putFloatField(target, fieldOffset, fieldValue);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public double getDouble(Object target) {
+        assert fieldType == double.class : field.getDeclaringClass() + "#" + field.getName() + " is not double";
+
+        return GridUnsafe.getDoubleField(target, fieldOffset);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setDouble(Object target, double fieldValue) {
+        assert fieldType == double.class : field.getDeclaringClass() + "#" + field.getName() + " is not double";
+
+        GridUnsafe.putDoubleField(target, fieldOffset, fieldValue);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public char getChar(Object target) {
+        assert fieldType == char.class : field.getDeclaringClass() + "#" + field.getName() + " is not char";
+
+        return GridUnsafe.getCharField(target, fieldOffset);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setChar(Object target, char fieldValue) {
+        assert fieldType == char.class : field.getDeclaringClass() + "#" + field.getName() + " is not char";
+
+        GridUnsafe.putCharField(target, fieldOffset, fieldValue);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean getBoolean(Object target) {
+        assert fieldType == boolean.class : field.getDeclaringClass() + "#" + field.getName() + " is not boolean";
+
+        return GridUnsafe.getBooleanField(target, fieldOffset);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void setBoolean(Object target, boolean fieldValue) {
+        assert fieldType == boolean.class : field.getDeclaringClass() + "#" + field.getName() + " is not boolean";
+
+        GridUnsafe.putBooleanField(target, fieldOffset, fieldValue);
+    }
+}
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshaller.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshaller.java
index 7842f91..4589e49 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshaller.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshaller.java
@@ -31,12 +31,11 @@ import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
 import java.util.Collection;
 import java.util.Map;
-import org.apache.ignite.internal.network.serialization.BuiltinType;
+import org.apache.ignite.internal.network.serialization.BuiltInType;
 import org.apache.ignite.internal.network.serialization.ClassDescriptor;
 import org.apache.ignite.internal.network.serialization.ClassDescriptorFactory;
 import org.apache.ignite.internal.network.serialization.ClassDescriptorFactoryContext;
 import org.apache.ignite.internal.network.serialization.IdIndexedDescriptors;
-import org.apache.ignite.internal.network.serialization.SerializedStreamCommands;
 import org.apache.ignite.internal.network.serialization.SpecialMethodInvocationException;
 import org.jetbrains.annotations.Nullable;
 
@@ -64,7 +63,7 @@ public class DefaultUserObjectMarshaller implements UserObjectMarshaller {
         this.localDescriptors = localDescriptors;
         this.descriptorFactory = descriptorFactory;
 
-        structuredObjectMarshaller = new StructuredObjectMarshaller(this::marshalToOutput, this::unmarshalFromInput);
+        structuredObjectMarshaller = new StructuredObjectMarshaller(localDescriptors, this::marshalToOutput, this::unmarshalFromInput);
 
         externalizableMarshaller = new ExternalizableMarshaller(
                 this::unmarshalFromInput,
@@ -227,7 +226,7 @@ public class DefaultUserObjectMarshaller implements UserObjectMarshaller {
         } else {
             // This is some custom class (not a built-in). If it's a non-built-in array, we need to handle it as a generic container.
             if (objectClass.isArray()) {
-                return localDescriptors.getBuiltInDescriptor(BuiltinType.OBJECT_ARRAY);
+                return localDescriptors.getBuiltInDescriptor(BuiltInType.OBJECT_ARRAY);
             }
 
             return descriptorFactory.create(objectClass);
@@ -239,7 +238,7 @@ public class DefaultUserObjectMarshaller implements UserObjectMarshaller {
     }
 
     private void writeReference(int objectId, DataOutput output) throws IOException {
-        ProtocolMarshalling.writeDescriptorOrCommandId(SerializedStreamCommands.REFERENCE, output);
+        ProtocolMarshalling.writeDescriptorOrCommandId(BuiltInType.REFERENCE.descriptorId(), output);
         ProtocolMarshalling.writeObjectId(objectId, output);
     }
 
@@ -314,7 +313,7 @@ public class DefaultUserObjectMarshaller implements UserObjectMarshaller {
 
     private <T> T unmarshalFromInput(DataInputStream input, UnmarshallingContext context) throws IOException, UnmarshalException {
         int commandOrDescriptorId = ProtocolMarshalling.readDescriptorOrCommandId(input);
-        if (commandOrDescriptorId == SerializedStreamCommands.REFERENCE) {
+        if (commandOrDescriptorId == BuiltInType.REFERENCE.descriptorId()) {
             return unmarshalReference(input, context);
         }
 
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 71d8d89..2e6278a 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
@@ -24,8 +24,11 @@ import java.io.ObjectInputStream;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import org.apache.ignite.internal.network.serialization.BuiltInTypeIds;
 import org.apache.ignite.internal.network.serialization.ClassDescriptor;
+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;
 
 /**
@@ -33,12 +36,14 @@ import org.apache.ignite.internal.network.serialization.SpecialMethodInvocationE
  * (which are not {@link java.io.Externalizable}s) and arbitrary (non-serializable, non-externalizable) objects.
  */
 class StructuredObjectMarshaller implements DefaultFieldsReaderWriter {
+    private final IdIndexedDescriptors descriptors;
     private final TypedValueWriter valueWriter;
     private final ValueReader<Object> valueReader;
 
     private final Instantiation instantiation;
 
-    StructuredObjectMarshaller(TypedValueWriter valueWriter, ValueReader<Object> valueReader) {
+    StructuredObjectMarshaller(IdIndexedDescriptors descriptors, TypedValueWriter valueWriter, ValueReader<Object> valueReader) {
+        this.descriptors = descriptors;
         this.valueWriter = valueWriter;
         this.valueReader = valueReader;
 
@@ -77,7 +82,7 @@ class StructuredObjectMarshaller implements DefaultFieldsReaderWriter {
 
     private void writeStructuredObjectLayer(Object object, ClassDescriptor layer, DataOutputStream output, MarshallingContext context)
             throws IOException, MarshalException {
-        if (layer.hasSerializationOverride()) {
+        if (layer.hasWriteObject()) {
             writeWithWriteObject(object, layer, output, context);
         } else {
             defaultWriteFields(object, layer, output, context);
@@ -113,9 +118,47 @@ class StructuredObjectMarshaller implements DefaultFieldsReaderWriter {
 
     private void writeField(Object object, FieldDescriptor fieldDescriptor, DataOutputStream output, MarshallingContext context)
             throws MarshalException, IOException {
-        Object fieldValue = fieldDescriptor.accessor().get(object);
+        if (fieldDescriptor.clazz().isPrimitive()) {
+            writePrimitiveFieldValue(object, fieldDescriptor, output);
 
-        valueWriter.write(fieldValue, fieldDescriptor.clazz(), output, context);
+            context.addUsedDescriptor(descriptors.getRequiredDescriptor(fieldDescriptor.typeDescriptorId()));
+        } else {
+            Object fieldValue = fieldDescriptor.accessor().getObject(object);
+            valueWriter.write(fieldValue, fieldDescriptor.clazz(), output, context);
+        }
+    }
+
+    private void writePrimitiveFieldValue(Object object, FieldDescriptor fieldDescriptor, DataOutputStream output) throws IOException {
+        FieldAccessor fieldAccessor = fieldDescriptor.accessor();
+
+        switch (fieldDescriptor.typeDescriptorId()) {
+            case BuiltInTypeIds.BYTE:
+                output.writeByte(fieldAccessor.getByte(object));
+                break;
+            case BuiltInTypeIds.SHORT:
+                output.writeShort(fieldAccessor.getShort(object));
+                break;
+            case BuiltInTypeIds.INT:
+                output.writeInt(fieldAccessor.getInt(object));
+                break;
+            case BuiltInTypeIds.LONG:
+                output.writeLong(fieldAccessor.getLong(object));
+                break;
+            case BuiltInTypeIds.FLOAT:
+                output.writeFloat(fieldAccessor.getFloat(object));
+                break;
+            case BuiltInTypeIds.DOUBLE:
+                output.writeDouble(fieldAccessor.getDouble(object));
+                break;
+            case BuiltInTypeIds.CHAR:
+                output.writeChar(fieldAccessor.getChar(object));
+                break;
+            case BuiltInTypeIds.BOOLEAN:
+                output.writeBoolean(fieldAccessor.getBoolean(object));
+                break;
+            default:
+                throw new IllegalStateException(fieldDescriptor.clazz() + " is primitive but not covered");
+        }
     }
 
     Object preInstantiateStructuredObject(ClassDescriptor descriptor) throws UnmarshalException {
@@ -135,7 +178,7 @@ class StructuredObjectMarshaller implements DefaultFieldsReaderWriter {
 
     private void fillStructuredObjectLayerFrom(DataInputStream input, ClassDescriptor layer, Object object, UnmarshallingContext context)
             throws IOException, UnmarshalException {
-        if (layer.hasSerializationOverride()) {
+        if (layer.hasReadObject()) {
             fillObjectWithReadObjectFrom(input, object, layer, context);
         } else {
             defaultFillFieldsFrom(input, object, layer, context);
@@ -166,13 +209,50 @@ class StructuredObjectMarshaller implements DefaultFieldsReaderWriter {
     public void defaultFillFieldsFrom(DataInputStream input, Object object, ClassDescriptor descriptor, UnmarshallingContext context)
             throws IOException, UnmarshalException {
         for (FieldDescriptor fieldDescriptor : descriptor.fields()) {
-            Object fieldValue = valueReader.read(input, context);
-            setFieldValue(object, fieldDescriptor, fieldValue);
+            fillFieldFrom(input, object, context, fieldDescriptor);
         }
     }
 
-    private void setFieldValue(Object target, FieldDescriptor fieldDescriptor, Object value) {
-        fieldDescriptor.accessor().set(target, value);
+    private void fillFieldFrom(DataInputStream input, Object object, UnmarshallingContext context, FieldDescriptor fieldDescriptor)
+            throws IOException, UnmarshalException {
+        if (fieldDescriptor.clazz().isPrimitive()) {
+            fillPrimitiveFieldFrom(input, object, fieldDescriptor);
+        } else {
+            Object fieldValue = valueReader.read(input, context);
+            fieldDescriptor.accessor().setObject(object, fieldValue);
+        }
     }
 
+    private void fillPrimitiveFieldFrom(DataInputStream input, Object object, FieldDescriptor fieldDescriptor) throws IOException {
+        FieldAccessor fieldAccessor = fieldDescriptor.accessor();
+
+        switch (fieldDescriptor.typeDescriptorId()) {
+            case BuiltInTypeIds.BYTE:
+                fieldAccessor.setByte(object, input.readByte());
+                break;
+            case BuiltInTypeIds.SHORT:
+                fieldAccessor.setShort(object, input.readShort());
+                break;
+            case BuiltInTypeIds.INT:
+                fieldAccessor.setInt(object, input.readInt());
+                break;
+            case BuiltInTypeIds.LONG:
+                fieldAccessor.setLong(object, input.readLong());
+                break;
+            case BuiltInTypeIds.FLOAT:
+                fieldAccessor.setFloat(object, input.readFloat());
+                break;
+            case BuiltInTypeIds.DOUBLE:
+                fieldAccessor.setDouble(object, input.readDouble());
+                break;
+            case BuiltInTypeIds.CHAR:
+                fieldAccessor.setChar(object, input.readChar());
+                break;
+            case BuiltInTypeIds.BOOLEAN:
+                fieldAccessor.setBoolean(object, input.readBoolean());
+                break;
+            default:
+                throw new IllegalStateException(fieldDescriptor.clazz() + " is primitive but not covered");
+        }
+    }
 }
diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactoryTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactoryTest.java
index d9adb3d..5bb4d62 100644
--- a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactoryTest.java
+++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/ClassDescriptorFactoryTest.java
@@ -331,7 +331,7 @@ public class ClassDescriptorFactoryTest {
         ClassDescriptor arbitraryDescriptor = context.getDescriptor(ArbitraryClass.class);
         assertNotNull(arbitraryDescriptor);
 
-        ClassDescriptor intDescriptor = context.getDescriptor(BuiltinType.INT.descriptorId());
+        ClassDescriptor intDescriptor = context.getDescriptor(BuiltInType.INT.descriptorId());
         assertNotNull(intDescriptor);
 
         List<FieldDescriptor> fields = holderDescriptor.fields();
@@ -398,6 +398,27 @@ public class ClassDescriptorFactoryTest {
         checkSerializable(descriptor.serialization(), true, true, true);
     }
 
+    @Test
+    void writeObjectMethodWithNonVoidReturnTypeIsIgnored() {
+        ClassDescriptor descriptor = factory.create(WithWriteObjectWithNonVoidReturnType.class);
+
+        assertFalse(descriptor.hasWriteObject());
+    }
+
+    @Test
+    void readObjectMethodWithNonVoidReturnTypeIsIgnored() {
+        ClassDescriptor descriptor = factory.create(WithReadObjectWithNonVoidReturnType.class);
+
+        assertFalse(descriptor.hasReadObject());
+    }
+
+    @Test
+    void readObjectNoDataMethodWithNonVoidReturnTypeIsIgnored() {
+        ClassDescriptor descriptor = factory.create(WithReadObjectNoDataWithNonVoidReturnType.class);
+
+        assertFalse(descriptor.hasReadObjectNoData());
+    }
+
     /**
      * Checks that serialization type is {@link SerializationType#ARBITRARY}.
      *
@@ -405,7 +426,11 @@ public class ClassDescriptorFactoryTest {
      */
     private void checkArbitraryType(Serialization serialization) {
         assertEquals(ARBITRARY, serialization.type());
-        assertFalse(serialization.hasSerializationOverride());
+
+        assertFalse(serialization.hasWriteObject());
+        assertFalse(serialization.hasReadObject());
+        assertFalse(serialization.hasReadObjectNoData());
+
         assertFalse(serialization.hasWriteReplace());
         assertFalse(serialization.hasReadResolve());
     }
@@ -426,7 +451,11 @@ public class ClassDescriptorFactoryTest {
      */
     private void checkSimpleExternalizable(Serialization serialization) {
         assertEquals(EXTERNALIZABLE, serialization.type());
-        assertFalse(serialization.hasSerializationOverride());
+
+        assertFalse(serialization.hasWriteObject());
+        assertFalse(serialization.hasReadObject());
+        assertFalse(serialization.hasReadObjectNoData());
+
         assertFalse(serialization.hasWriteReplace());
         assertFalse(serialization.hasReadResolve());
     }
@@ -442,7 +471,9 @@ public class ClassDescriptorFactoryTest {
     private void checkSerializable(Serialization serialization, boolean override, boolean writeReplace, boolean readResolve) {
         assertEquals(SERIALIZABLE, serialization.type());
 
-        assertEquals(override, serialization.hasSerializationOverride());
+        assertEquals(override, serialization.hasWriteObject());
+        assertEquals(override, serialization.hasReadObject());
+
         assertEquals(writeReplace, serialization.hasWriteReplace());
         assertEquals(readResolve, serialization.hasReadResolve());
     }
@@ -512,4 +543,25 @@ public class ClassDescriptorFactoryTest {
 
     private static class ExtendsObject {
     }
+
+    private static class WithWriteObjectWithNonVoidReturnType implements Serializable {
+        @SuppressWarnings("unused")
+        private Object writeObject(ObjectOutputStream stream) {
+            return null;
+        }
+    }
+
+    private static class WithReadObjectWithNonVoidReturnType implements Serializable {
+        @SuppressWarnings("unused")
+        private Object readObject(ObjectInputStream stream) {
+            return null;
+        }
+    }
+
+    private static class WithReadObjectNoDataWithNonVoidReturnType implements Serializable {
+        @SuppressWarnings("unused")
+        private Object readObjectNoData(ObjectInputStream stream) {
+            return null;
+        }
+    }
 }
diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/DefaultDescriptorsTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/DefaultDescriptorsTest.java
index 1faf851..0a29655 100644
--- a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/DefaultDescriptorsTest.java
+++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/DefaultDescriptorsTest.java
@@ -17,50 +17,51 @@
 
 package org.apache.ignite.internal.network.serialization;
 
-import static org.apache.ignite.internal.network.serialization.BuiltinType.ARRAY_LIST;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.BARE_OBJECT;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.BIT_SET;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.BOOLEAN;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.BOOLEAN_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.BOOLEAN_BOXED;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.BYTE;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.BYTE_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.BYTE_BOXED;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.CHAR;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.CHAR_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.CHAR_BOXED;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.DATE;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.DECIMAL;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.DECIMAL_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.DOUBLE;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.DOUBLE_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.DOUBLE_BOXED;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.ENUM;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.ENUM_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.FLOAT;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.FLOAT_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.FLOAT_BOXED;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.HASH_MAP;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.HASH_SET;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.IGNITE_UUID;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.INT;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.INT_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.INT_BOXED;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.LINKED_HASH_MAP;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.LINKED_HASH_SET;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.LINKED_LIST;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.LONG;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.LONG_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.LONG_BOXED;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.NULL;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.OBJECT_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.SHORT;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.SHORT_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.SHORT_BOXED;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.SINGLETON_LIST;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.STRING;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.STRING_ARRAY;
-import static org.apache.ignite.internal.network.serialization.BuiltinType.UUID;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.ARRAY_LIST;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.BARE_OBJECT;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.BIT_SET;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.BOOLEAN;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.BOOLEAN_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.BOOLEAN_BOXED;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.BYTE;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.BYTE_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.BYTE_BOXED;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.CHAR;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.CHAR_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.CHAR_BOXED;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.DATE;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.DECIMAL;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.DECIMAL_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.DOUBLE;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.DOUBLE_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.DOUBLE_BOXED;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.ENUM;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.ENUM_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.FLOAT;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.FLOAT_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.FLOAT_BOXED;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.HASH_MAP;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.HASH_SET;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.IGNITE_UUID;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.INT;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.INT_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.INT_BOXED;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.LINKED_HASH_MAP;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.LINKED_HASH_SET;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.LINKED_LIST;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.LONG;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.LONG_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.LONG_BOXED;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.NULL;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.OBJECT_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.REFERENCE;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.SHORT;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.SHORT_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.SHORT_BOXED;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.SINGLETON_LIST;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.STRING;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.STRING_ARRAY;
+import static org.apache.ignite.internal.network.serialization.BuiltInType.UUID;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import org.junit.jupiter.api.Test;
@@ -118,5 +119,6 @@ public class DefaultDescriptorsTest {
         assertEquals(41, LINKED_HASH_MAP.descriptorId());
         assertEquals(42, BIT_SET.descriptorId());
         assertEquals(43, NULL.descriptorId());
+        assertEquals(44, REFERENCE.descriptorId());
     }
 }
diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithArbitraryObjectsTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithArbitraryObjectsTest.java
index b05ea35..c176151 100644
--- a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithArbitraryObjectsTest.java
+++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithArbitraryObjectsTest.java
@@ -26,6 +26,7 @@ import static org.hamcrest.Matchers.hasItems;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.instanceOf;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.not;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.Matchers.sameInstance;
@@ -38,11 +39,12 @@ import java.io.DataInputStream;
 import java.io.IOException;
 import java.io.Serializable;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.Callable;
-import org.apache.ignite.internal.network.serialization.BuiltinType;
+import org.apache.ignite.internal.network.serialization.BuiltInType;
 import org.apache.ignite.internal.network.serialization.ClassDescriptorFactory;
 import org.apache.ignite.internal.network.serialization.ClassDescriptorFactoryContext;
 import org.apache.ignite.internal.network.serialization.IdIndexedDescriptors;
@@ -91,7 +93,7 @@ class DefaultUserObjectMarshallerWithArbitraryObjectsTest {
 
         assertThat(marshalled.usedDescriptors(), equalTo(Set.of(
                 descriptorRegistry.getRequiredDescriptor(Simple.class),
-                descriptorRegistry.getBuiltInDescriptor(BuiltinType.INT)
+                descriptorRegistry.getBuiltInDescriptor(BuiltInType.INT)
         )));
     }
 
@@ -410,6 +412,56 @@ class DefaultUserObjectMarshallerWithArbitraryObjectsTest {
         assertThat(unmarshalled.value, is(EnumWithAnonClassesForMembers.FIRST));
     }
 
+    @Test
+    void marshalsAndUnmarshalsPrimitivesInFieldsCorrectly() throws Exception {
+        WithPrimitives unmarshalled = marshalAndUnmarshalNonNull(new WithPrimitives());
+
+        assertThat(unmarshalled.byteVal, is((byte) 1));
+        assertThat(unmarshalled.shortVal, is((short) 2));
+        assertThat(unmarshalled.intVal, is(3));
+        assertThat(unmarshalled.longVal, is(4L));
+        assertThat(unmarshalled.floatVal, is(5.0f));
+        assertThat(unmarshalled.doubleVal, is(6.0));
+        assertThat(unmarshalled.charVal, is('a'));
+        assertThat(unmarshalled.booleanVal, is(true));
+    }
+
+    @Test
+    void marshalsAndUnmarshalsPrimitiveWrappersInFieldsCorrectly() throws Exception {
+        WithPrimitiveWrappers unmarshalled = marshalAndUnmarshalNonNull(new WithPrimitiveWrappers());
+
+        assertThat(unmarshalled.byteVal, is((byte) 1));
+        assertThat(unmarshalled.shortVal, is((short) 2));
+        assertThat(unmarshalled.integerVal, is(3));
+        assertThat(unmarshalled.longVal, is(4L));
+        assertThat(unmarshalled.floatVal, is(5.0f));
+        assertThat(unmarshalled.doubleVal, is(6.0));
+        assertThat(unmarshalled.characterVal, is('a'));
+        assertThat(unmarshalled.booleanVal, is(true));
+    }
+
+    @Test
+    void unmarshalsReferencesToSameObjectOfNonBuiltInTypeToSameObject() throws Exception {
+        Simple obj = new Simple(42);
+        List<?> list = new ArrayList<>(Arrays.asList(obj, obj));
+
+        List<?> unmarshalled = marshalAndUnmarshalNonNull(list);
+
+        assertThat(unmarshalled.get(0), sameInstance(unmarshalled.get(1)));
+    }
+
+    @Test
+    void unmarshalsDifferentButEqualObjectsToDifferentObjects() throws Exception {
+        long longValue = 1_000_000;
+        String obj1 = String.valueOf(longValue);
+        String obj2 = String.valueOf(longValue);
+        List<?> list = new ArrayList<>(Arrays.asList(obj1, obj2));
+
+        List<?> unmarshalled = marshalAndUnmarshalNonNull(list);
+
+        assertThat(unmarshalled.get(0), not(sameInstance(unmarshalled.get(1))));
+    }
+
     private static class Simple {
         private int value;
 
@@ -566,4 +618,26 @@ class DefaultUserObjectMarshallerWithArbitraryObjectsTest {
             this.value = value;
         }
     }
+
+    private static class WithPrimitives {
+        private final byte byteVal = 1;
+        private final short shortVal = 2;
+        private final int intVal = 3;
+        private final long longVal = 4L;
+        private final float floatVal = 5.0f;
+        private final double doubleVal = 6.0;
+        private final char charVal = 'a';
+        private final boolean booleanVal = true;
+    }
+
+    private static class WithPrimitiveWrappers {
+        private final Byte byteVal = 1;
+        private final Short shortVal = 2;
+        private final Integer integerVal = 3;
+        private final Long longVal = 4L;
+        private final Float floatVal = 5.0f;
+        private final Double doubleVal = 6.0;
+        private final Character characterVal = 'a';
+        private final Boolean booleanVal = true;
+    }
 }
diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithBuiltinsTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithBuiltinsTest.java
index 3c25458..a388f0a 100644
--- a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithBuiltinsTest.java
+++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithBuiltinsTest.java
@@ -47,7 +47,7 @@ import java.util.function.BiConsumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
 import java.util.stream.Stream;
-import org.apache.ignite.internal.network.serialization.BuiltinType;
+import org.apache.ignite.internal.network.serialization.BuiltInType;
 import org.apache.ignite.internal.network.serialization.ClassDescriptor;
 import org.apache.ignite.internal.network.serialization.ClassDescriptorFactory;
 import org.apache.ignite.internal.network.serialization.ClassDescriptorFactoryContext;
@@ -97,7 +97,7 @@ class DefaultUserObjectMarshallerWithBuiltinsTest {
     void marshalsBareObjectWithCorrectDescriptorIdInMarshalledRepresentation() throws Exception {
         MarshalledObject marshalled = marshaller.marshal(new Object());
 
-        assertThat(readDescriptorId(marshalled), is(BuiltinType.BARE_OBJECT.descriptorId()));
+        assertThat(readDescriptorId(marshalled), is(BuiltInType.BARE_OBJECT.descriptorId()));
     }
 
     private int readDescriptorId(MarshalledObject marshalled) throws IOException {
@@ -141,7 +141,7 @@ class DefaultUserObjectMarshallerWithBuiltinsTest {
         Object unmarshalled = marshaller.unmarshal(marshalled.bytes(), descriptors);
 
         assertThat(unmarshalled, is(equalTo(typeValue.value)));
-        if (typeValue.builtinType != BuiltinType.NULL && typeValue.value.getClass().isArray()) {
+        if (typeValue.builtinType != BuiltInType.NULL && typeValue.value.getClass().isArray()) {
             assertThat(unmarshalled, is(notNullValue()));
             assertThat(unmarshalled.getClass().getComponentType(), is(typeValue.value.getClass().getComponentType()));
         }
@@ -152,7 +152,7 @@ class DefaultUserObjectMarshallerWithBuiltinsTest {
     void marshalsUsingOnlyCorrespondingDescriptorForBuiltInNonCollectionTypes(BuiltInTypeValue typeValue) {
         // #marshalsObjectArrayUsingExactlyDescriptorsOfObjectArrayAndComponents() checks the same for OBJECT_ARRAY
 
-        assumingThat(typeValue.builtinType != BuiltinType.OBJECT_ARRAY, () -> {
+        assumingThat(typeValue.builtinType != BuiltInType.OBJECT_ARRAY, () -> {
             MarshalledObject marshalled = marshaller.marshal(typeValue.value, typeValue.valueClass);
 
             ClassDescriptor expectedDescriptor = descriptorRegistry.getBuiltInDescriptor(typeValue.builtinType);
@@ -162,56 +162,56 @@ class DefaultUserObjectMarshallerWithBuiltinsTest {
 
     static Stream<Arguments> builtInNonCollectionTypes() {
         return Stream.of(
-                builtInTypeValue((byte) 42, byte.class, BuiltinType.BYTE),
-                builtInTypeValue((byte) 42, Byte.class, BuiltinType.BYTE_BOXED),
-                builtInTypeValue((short) 42, short.class, BuiltinType.SHORT),
-                builtInTypeValue((short) 42, Short.class, BuiltinType.SHORT_BOXED),
-                builtInTypeValue(42, int.class, BuiltinType.INT),
-                builtInTypeValue(42, Integer.class, BuiltinType.INT_BOXED),
-                builtInTypeValue(42.0f, float.class, BuiltinType.FLOAT),
-                builtInTypeValue(42.0f, Float.class, BuiltinType.FLOAT_BOXED),
-                builtInTypeValue((long) 42, long.class, BuiltinType.LONG),
-                builtInTypeValue((long) 42, Long.class, BuiltinType.LONG_BOXED),
-                builtInTypeValue(42.0, double.class, BuiltinType.DOUBLE),
-                builtInTypeValue(42.0, Double.class, BuiltinType.DOUBLE_BOXED),
-                builtInTypeValue(true, boolean.class, BuiltinType.BOOLEAN),
-                builtInTypeValue(true, Boolean.class, BuiltinType.BOOLEAN_BOXED),
-                builtInTypeValue('a', char.class, BuiltinType.CHAR),
-                builtInTypeValue('a', Character.class, BuiltinType.CHAR_BOXED),
+                builtInTypeValue((byte) 42, byte.class, BuiltInType.BYTE),
+                builtInTypeValue((byte) 42, Byte.class, BuiltInType.BYTE_BOXED),
+                builtInTypeValue((short) 42, short.class, BuiltInType.SHORT),
+                builtInTypeValue((short) 42, Short.class, BuiltInType.SHORT_BOXED),
+                builtInTypeValue(42, int.class, BuiltInType.INT),
+                builtInTypeValue(42, Integer.class, BuiltInType.INT_BOXED),
+                builtInTypeValue(42.0f, float.class, BuiltInType.FLOAT),
+                builtInTypeValue(42.0f, Float.class, BuiltInType.FLOAT_BOXED),
+                builtInTypeValue((long) 42, long.class, BuiltInType.LONG),
+                builtInTypeValue((long) 42, Long.class, BuiltInType.LONG_BOXED),
+                builtInTypeValue(42.0, double.class, BuiltInType.DOUBLE),
+                builtInTypeValue(42.0, Double.class, BuiltInType.DOUBLE_BOXED),
+                builtInTypeValue(true, boolean.class, BuiltInType.BOOLEAN),
+                builtInTypeValue(true, Boolean.class, BuiltInType.BOOLEAN_BOXED),
+                builtInTypeValue('a', char.class, BuiltInType.CHAR),
+                builtInTypeValue('a', Character.class, BuiltInType.CHAR_BOXED),
                 // BARE_OBJECT is handled separately
-                builtInTypeValue("abc", String.class, BuiltinType.STRING),
-                builtInTypeValue(UUID.fromString("c6f57d4a-619f-11ec-add6-73bc97c3c49e"), UUID.class, BuiltinType.UUID),
+                builtInTypeValue("abc", String.class, BuiltInType.STRING),
+                builtInTypeValue(UUID.fromString("c6f57d4a-619f-11ec-add6-73bc97c3c49e"), UUID.class, BuiltInType.UUID),
                 builtInTypeValue(IgniteUuid.fromString("1234-c6f57d4a-619f-11ec-add6-73bc97c3c49e"), IgniteUuid.class,
-                        BuiltinType.IGNITE_UUID),
-                builtInTypeValue(new Date(42), Date.class, BuiltinType.DATE),
-                builtInTypeValue(new byte[]{1, 2, 3}, byte[].class, BuiltinType.BYTE_ARRAY),
-                builtInTypeValue(new short[]{1, 2, 3}, short[].class, BuiltinType.SHORT_ARRAY),
-                builtInTypeValue(new int[]{1, 2, 3}, int[].class, BuiltinType.INT_ARRAY),
-                builtInTypeValue(new float[]{1.0f, 2.0f, 3.0f}, float[].class, BuiltinType.FLOAT_ARRAY),
-                builtInTypeValue(new long[]{1, 2, 3}, long[].class, BuiltinType.LONG_ARRAY),
-                builtInTypeValue(new double[]{1.0, 2.0, 3.0}, double[].class, BuiltinType.DOUBLE_ARRAY),
-                builtInTypeValue(new boolean[]{true, false}, boolean[].class, BuiltinType.BOOLEAN_ARRAY),
-                builtInTypeValue(new char[]{'a', 'b'}, char[].class, BuiltinType.CHAR_ARRAY),
-                builtInTypeValue(new Object[]{42, "123", null}, Object[].class, BuiltinType.OBJECT_ARRAY),
+                        BuiltInType.IGNITE_UUID),
+                builtInTypeValue(new Date(42), Date.class, BuiltInType.DATE),
+                builtInTypeValue(new byte[]{1, 2, 3}, byte[].class, BuiltInType.BYTE_ARRAY),
+                builtInTypeValue(new short[]{1, 2, 3}, short[].class, BuiltInType.SHORT_ARRAY),
+                builtInTypeValue(new int[]{1, 2, 3}, int[].class, BuiltInType.INT_ARRAY),
+                builtInTypeValue(new float[]{1.0f, 2.0f, 3.0f}, float[].class, BuiltInType.FLOAT_ARRAY),
+                builtInTypeValue(new long[]{1, 2, 3}, long[].class, BuiltInType.LONG_ARRAY),
+                builtInTypeValue(new double[]{1.0, 2.0, 3.0}, double[].class, BuiltInType.DOUBLE_ARRAY),
+                builtInTypeValue(new boolean[]{true, false}, boolean[].class, BuiltInType.BOOLEAN_ARRAY),
+                builtInTypeValue(new char[]{'a', 'b'}, char[].class, BuiltInType.CHAR_ARRAY),
+                builtInTypeValue(new Object[]{42, "123", null}, Object[].class, BuiltInType.OBJECT_ARRAY),
                 builtInTypeValue(new BitSet[]{BitSet.valueOf(new long[]{42, 43}), BitSet.valueOf(new long[]{1, 2}), null},
-                        BitSet[].class, BuiltinType.OBJECT_ARRAY),
-                builtInTypeValue(new String[]{"Ignite", "rulez"}, String[].class, BuiltinType.STRING_ARRAY),
-                builtInTypeValue(new BigDecimal(42), BigDecimal.class, BuiltinType.DECIMAL),
+                        BitSet[].class, BuiltInType.OBJECT_ARRAY),
+                builtInTypeValue(new String[]{"Ignite", "rulez"}, String[].class, BuiltInType.STRING_ARRAY),
+                builtInTypeValue(new BigDecimal(42), BigDecimal.class, BuiltInType.DECIMAL),
                 builtInTypeValue(new BigDecimal[]{new BigDecimal(42), new BigDecimal(43)}, BigDecimal[].class,
-                        BuiltinType.DECIMAL_ARRAY),
-                builtInTypeValue(SimpleEnum.FIRST, SimpleEnum.class, BuiltinType.ENUM),
-                builtInTypeValue(new Enum[]{SimpleEnum.FIRST, SimpleEnum.SECOND}, Enum[].class, BuiltinType.ENUM_ARRAY),
-                builtInTypeValue(new SimpleEnum[]{SimpleEnum.FIRST, SimpleEnum.SECOND}, SimpleEnum[].class, BuiltinType.ENUM_ARRAY),
-                builtInTypeValue(EnumWithAnonClassesForMembers.FIRST, EnumWithAnonClassesForMembers.class, BuiltinType.ENUM),
+                        BuiltInType.DECIMAL_ARRAY),
+                builtInTypeValue(SimpleEnum.FIRST, SimpleEnum.class, BuiltInType.ENUM),
+                builtInTypeValue(new Enum[]{SimpleEnum.FIRST, SimpleEnum.SECOND}, Enum[].class, BuiltInType.ENUM_ARRAY),
+                builtInTypeValue(new SimpleEnum[]{SimpleEnum.FIRST, SimpleEnum.SECOND}, SimpleEnum[].class, BuiltInType.ENUM_ARRAY),
+                builtInTypeValue(EnumWithAnonClassesForMembers.FIRST, EnumWithAnonClassesForMembers.class, BuiltInType.ENUM),
                 builtInTypeValue(new Enum[]{EnumWithAnonClassesForMembers.FIRST, EnumWithAnonClassesForMembers.SECOND}, Enum[].class,
-                        BuiltinType.ENUM_ARRAY),
+                        BuiltInType.ENUM_ARRAY),
                 builtInTypeValue(
                         new EnumWithAnonClassesForMembers[]{EnumWithAnonClassesForMembers.FIRST, EnumWithAnonClassesForMembers.SECOND},
                         EnumWithAnonClassesForMembers[].class,
-                        BuiltinType.ENUM_ARRAY
+                        BuiltInType.ENUM_ARRAY
                 ),
-                builtInTypeValue(BitSet.valueOf(new long[]{42, 43}), BitSet.class, BuiltinType.BIT_SET),
-                builtInTypeValue(null, Null.class, BuiltinType.NULL)
+                builtInTypeValue(BitSet.valueOf(new long[]{42, 43}), BitSet.class, BuiltInType.BIT_SET),
+                builtInTypeValue(null, Null.class, BuiltInType.NULL)
         ).map(Arguments::of);
     }
 
@@ -242,19 +242,19 @@ class DefaultUserObjectMarshallerWithBuiltinsTest {
 
         assertThat(marshalled.usedDescriptors(), containsInAnyOrder(
                 descriptorRegistry.getBuiltInDescriptor(typeValue.builtinType),
-                descriptorRegistry.getBuiltInDescriptor(BuiltinType.INT_BOXED)
+                descriptorRegistry.getBuiltInDescriptor(BuiltInType.INT_BOXED)
         ));
     }
 
     static Stream<Arguments> builtInCollectionTypes() {
         return Stream.of(
-                builtInTypeValue(new ArrayList<>(List.of(42, 43)), ArrayList.class, BuiltinType.ARRAY_LIST),
-                builtInTypeValue(new LinkedList<>(List.of(42, 43)), LinkedList.class, BuiltinType.LINKED_LIST),
-                builtInTypeValue(new HashSet<>(Set.of(42, 43)), HashSet.class, BuiltinType.HASH_SET),
-                builtInTypeValue(new LinkedHashSet<>(Set.of(42, 43)), LinkedHashSet.class, BuiltinType.LINKED_HASH_SET),
-                builtInTypeValue(singletonList(42), BuiltinType.SINGLETON_LIST.clazz(), BuiltinType.SINGLETON_LIST),
-                builtInTypeValue(new HashMap<>(Map.of(42, 43)), HashMap.class, BuiltinType.HASH_MAP),
-                builtInTypeValue(new LinkedHashMap<>(Map.of(42, 43)), LinkedHashMap.class, BuiltinType.LINKED_HASH_MAP)
+                builtInTypeValue(new ArrayList<>(List.of(42, 43)), ArrayList.class, BuiltInType.ARRAY_LIST),
+                builtInTypeValue(new LinkedList<>(List.of(42, 43)), LinkedList.class, BuiltInType.LINKED_LIST),
+                builtInTypeValue(new HashSet<>(Set.of(42, 43)), HashSet.class, BuiltInType.HASH_SET),
+                builtInTypeValue(new LinkedHashSet<>(Set.of(42, 43)), LinkedHashSet.class, BuiltInType.LINKED_HASH_SET),
+                builtInTypeValue(singletonList(42), BuiltInType.SINGLETON_LIST.clazz(), BuiltInType.SINGLETON_LIST),
+                builtInTypeValue(new HashMap<>(Map.of(42, 43)), HashMap.class, BuiltInType.HASH_MAP),
+                builtInTypeValue(new LinkedHashMap<>(Map.of(42, 43)), LinkedHashMap.class, BuiltInType.LINKED_HASH_MAP)
         ).map(Arguments::of);
     }
 
@@ -270,7 +270,7 @@ class DefaultUserObjectMarshallerWithBuiltinsTest {
         return Stream.concat(builtInNonCollectionTypes(), builtInCollectionTypes());
     }
 
-    private static BuiltInTypeValue builtInTypeValue(Object value, Class<?> valueClass, BuiltinType type) {
+    private static BuiltInTypeValue builtInTypeValue(Object value, Class<?> valueClass, BuiltInType type) {
         return new BuiltInTypeValue(value, valueClass, type);
     }
 
@@ -333,9 +333,9 @@ class DefaultUserObjectMarshallerWithBuiltinsTest {
     private static class BuiltInTypeValue {
         private final Object value;
         private final Class<?> valueClass;
-        private final BuiltinType builtinType;
+        private final BuiltInType builtinType;
 
-        private BuiltInTypeValue(Object value, Class<?> valueClass, BuiltinType builtinType) {
+        private BuiltInTypeValue(Object value, Class<?> valueClass, BuiltInType builtinType) {
             this.value = value;
             this.valueClass = valueClass;
             this.builtinType = builtinType;
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 74aee61..d8f91f9 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
@@ -37,7 +37,6 @@ import java.io.ObjectOutputStream;
 import java.io.Serializable;
 import java.util.Objects;
 import java.util.stream.Stream;
-import org.apache.ignite.internal.network.serialization.BuiltinType;
 import org.apache.ignite.internal.network.serialization.ClassDescriptorFactory;
 import org.apache.ignite.internal.network.serialization.ClassDescriptorFactoryContext;
 import org.apache.ignite.internal.network.serialization.IdIndexedDescriptors;
@@ -332,7 +331,6 @@ class DefaultUserObjectMarshallerWithSerializableOverrideStreamsTest {
         byte[] overrideBytes = readOverrideBytes(marshalled);
         DataInputStream dis = new DataInputStream(new ByteArrayInputStream(overrideBytes));
 
-        assertThat(ProtocolMarshalling.readDescriptorOrCommandId(dis), is(BuiltinType.INT.descriptorId()));
         assertThat(dis.readInt(), is(42));
         assertThatDrained(dis);
     }
diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSerializableTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSerializableTest.java
index b1cbc8e..898ac81 100644
--- a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSerializableTest.java
+++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerWithSerializableTest.java
@@ -63,6 +63,8 @@ class DefaultUserObjectMarshallerWithSerializableTest {
 
     private static boolean nonSerializableParentConstructorCalled;
     private static boolean constructorCalled;
+    private static boolean writeObjectCalled;
+    private static boolean readObjectCalled;
 
     @Test
     void marshalsAndUnmarshalsSerializable() throws Exception {
@@ -239,6 +241,20 @@ class DefaultUserObjectMarshallerWithSerializableTest {
         assertThat(deserialized.ref.ref, is(not(sameInstance(deserialized))));
     }
 
+    @Test
+    void invokesWriteObjectEvenWhenThereIsNoReadObject() throws Exception {
+        marshalAndUnmarshalNonNull(new WithWriteObjectButNoReadObject());
+
+        assertTrue(writeObjectCalled);
+    }
+
+    @Test
+    void invokesReadObjectEvenWhenThereIsNoWriteObject() throws Exception {
+        marshalAndUnmarshalNonNull(new WithReadObjectButNoWriteObject());
+
+        assertTrue(readObjectCalled);
+    }
+
     /**
      * An {@link Serializable} that does not have {@code writeReplace()}/{@code readResolve()} methods or other customizations.
      */
@@ -482,4 +498,16 @@ class DefaultUserObjectMarshallerWithSerializableTest {
             return new IndirectSelfRefWithResolveToSelf(value + READ_RESOLVE_INCREMENT, ref);
         }
     }
+
+    private static class WithWriteObjectButNoReadObject implements Serializable {
+        private void writeObject(ObjectOutputStream stream) {
+            writeObjectCalled = true;
+        }
+    }
+
+    private static class WithReadObjectButNoWriteObject implements Serializable {
+        private void readObject(ObjectInputStream stream) {
+            readObjectCalled = true;
+        }
+    }
 }