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 2021/12/22 09:16:05 UTC

[ignite-3] branch main updated: IGNITE-16154 Implement (un)marshalling of Externalizable

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 e953d0c  IGNITE-16154 Implement (un)marshalling of Externalizable
e953d0c is described below

commit e953d0c9ab4f9d1a4802300557c27855f2a97218
Author: Roman Puchkovskiy <ro...@gmail.com>
AuthorDate: Sun Dec 19 16:52:57 2021 +0400

    IGNITE-16154 Implement (un)marshalling of Externalizable
---
 .../{DefaultType.java => BuiltinType.java}         |  14 +-
 .../network/serialization/ClassDescriptor.java     |  58 ++++++-
 .../serialization/ClassDescriptorFactory.java      | 108 ++++--------
 .../ClassDescriptorFactoryContext.java             |  16 +-
 .../network/serialization/Serialization.java       |  97 +++++++++++
 .../network/serialization/SerializationType.java   |  32 ++--
 .../marshal/DefaultUserObjectMarshaller.java       | 181 +++++++++++++++++++++
 .../serialization/marshal/MarshalException.java    |  29 ++++
 .../serialization/marshal/MarshalledObject.java    |  67 ++++++++
 .../serialization/marshal/ReflectionException.java |  30 ++++
 .../marshal/SpecialSerializationMethods.java       | 151 +++++++++++++++++
 .../marshal/SpecialSerializationMethodsCache.java  |  33 ++++
 .../serialization/marshal/UnmarshalException.java  |  29 ++++
 .../marshal/UserObjectMarshaller.java              |  44 +++++
 .../serialization/ClassDescriptorFactoryTest.java  |  91 +++++------
 .../serialization/DefaultDescriptorsTest.java      |  90 +++++-----
 .../marshal/DefaultUserObjectMarshallerTest.java   | 159 ++++++++++++++++++
 17 files changed, 1031 insertions(+), 198 deletions(-)

diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/DefaultType.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltinType.java
similarity index 93%
rename from modules/network/src/main/java/org/apache/ignite/internal/network/serialization/DefaultType.java
rename to modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltinType.java
index 8b10b11..2094d83 100644
--- a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/DefaultType.java
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/BuiltinType.java
@@ -30,9 +30,9 @@ import java.util.LinkedList;
 import org.apache.ignite.lang.IgniteUuid;
 
 /**
- * Default types.
+ * Built-in types.
  */
-public enum DefaultType {
+public enum BuiltinType {
     BYTE(0, byte.class),
     BYTE_BOXED(1, Byte.class),
     SHORT(2, short.class),
@@ -95,7 +95,7 @@ public enum DefaultType {
      * @param descriptorId Descriptor id.
      * @param clazz        Type.
      */
-    DefaultType(int descriptorId, Class<?> clazz) {
+    BuiltinType(int descriptorId, Class<?> clazz) {
         this.descriptorId = descriptorId;
         this.clazz = clazz;
     }
@@ -125,10 +125,10 @@ public enum DefaultType {
      */
     public ClassDescriptor asClassDescriptor() {
         return new ClassDescriptor(
-            clazz,
-            descriptorId,
-            Collections.emptyList(),
-            SerializationType.DEFAULT
+                clazz,
+                descriptorId,
+                Collections.emptyList(),
+                new Serialization(SerializationType.BUILTIN)
         );
     }
 }
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 78f2873..0b80c82 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
@@ -49,9 +49,9 @@ public class ClassDescriptor {
     private final List<FieldDescriptor> fields;
 
     /**
-     * The type of the serialization mechanism for the class.
+     * How the class is to be serialized.
      */
-    private final int serializationType;
+    private final Serialization serialization;
 
     /**
      * Whether the class is final.
@@ -61,12 +61,12 @@ public class ClassDescriptor {
     /**
      * Constructor.
      */
-    public ClassDescriptor(@NotNull Class<?> clazz, int descriptorId, @NotNull List<FieldDescriptor> fields, int serializationType) {
+    public ClassDescriptor(@NotNull Class<?> clazz, int descriptorId, @NotNull List<FieldDescriptor> fields, Serialization serialization) {
         this.className = clazz.getName();
         this.clazz = clazz;
         this.descriptorId = descriptorId;
         this.fields = List.copyOf(fields);
-        this.serializationType = serializationType;
+        this.serialization = serialization;
         this.isFinal = Modifier.isFinal(clazz.getModifiers());
     }
 
@@ -110,12 +110,21 @@ public class ClassDescriptor {
     }
 
     /**
+     * Returns serialization.
+     *
+     * @return Serialization.
+     */
+    public Serialization serialization() {
+        return serialization;
+    }
+
+    /**
      * Returns serialization type.
      *
      * @return Serialization type.
      */
-    public int serializationType() {
-        return serializationType;
+    public SerializationType serializationType() {
+        return serialization.type();
     }
 
     /**
@@ -126,4 +135,41 @@ public class ClassDescriptor {
     public boolean isFinal() {
         return isFinal;
     }
+
+    /**
+     * Returns {@code true} if the described class should be serialized as a {@link java.io.Serializable} (but not
+     * using the mechanism for {@link java.io.Externalizable}).
+     *
+     * @return {@code true} if the described class should be serialized as a {@link java.io.Serializable}.
+     */
+    public boolean isSerializable() {
+        return serialization.type() == SerializationType.SERIALIZABLE;
+    }
+
+    /**
+     * Returns {@code true} if the described class should be serialized as an {@link java.io.Externalizable}.
+     *
+     * @return {@code true} if the described class should be serialized as an {@link java.io.Externalizable}.
+     */
+    public boolean isExternalizable() {
+        return serialization.type() == SerializationType.EXTERNALIZABLE;
+    }
+
+    /**
+     * Returns {@code true} if the described class has {@code writeReplace()} method.
+     *
+     * @return {@code true} if the described class has {@code writeReplace()} method
+     */
+    public boolean hasWriteReplace() {
+        return serialization.hasWriteReplace();
+    }
+
+    /**
+     * Returns {@code true} if the described class has {@code readResolve()} method.
+     *
+     * @return {@code true} if the described class has {@code readResolve()} method
+     */
+    public boolean hasReadResolve() {
+        return serialization.hasReadResolve();
+    }
 }
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 9e345ae..31fe206 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
@@ -18,10 +18,8 @@
 package org.apache.ignite.internal.network.serialization;
 
 import java.io.Externalizable;
-import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
-import java.io.ObjectStreamException;
 import java.io.Serializable;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
@@ -31,7 +29,6 @@ import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Queue;
-import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.ignite.lang.IgniteException;
 import org.jetbrains.annotations.Nullable;
@@ -40,6 +37,9 @@ 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;
+
     /**
      * Factory context.
      */
@@ -122,10 +122,15 @@ public class ClassDescriptorFactory {
         checkHasPublicNoArgConstructor(clazz);
 
         return new ClassDescriptor(
-            clazz,
-            descriptorId,
-            Collections.emptyList(),
-            SerializationType.EXTERNALIZABLE
+                clazz,
+                descriptorId,
+                Collections.emptyList(),
+                new Serialization(
+                        SerializationType.EXTERNALIZABLE,
+                        NO_SERIALIZATION_OVERRIDE,
+                        hasWriteReplace(clazz),
+                        hasReadResolve(clazz)
+                )
         );
     }
 
@@ -161,30 +166,30 @@ public class ClassDescriptorFactory {
      * @return Class descriptor.
      */
     private ClassDescriptor serializable(int descriptorId, Class<? extends Serializable> clazz) {
-        Method writeObject = getWriteObject(clazz);
-        Method readObject = getReadObject(clazz);
-        Method readObjectNoData = getReadObjectNoData(clazz);
-
-        boolean overrideSerialization = writeObject != null && readObject != null && readObjectNoData != null;
-
-        Method writeReplace = getWriteReplace(clazz);
-        Method readResolve = getReadResolve(clazz);
-
-        int serializationType = SerializationType.SERIALIZABLE;
+        return new ClassDescriptor(clazz, descriptorId, fields(clazz),
+                new Serialization(
+                        SerializationType.SERIALIZABLE,
+                        hasOverrideSerialization(clazz),
+                        hasWriteReplace(clazz),
+                        hasReadResolve(clazz)
+                )
+        );
+    }
 
-        if (overrideSerialization) {
-            serializationType |= SerializationType.SERIALIZABLE_OVERRIDE;
-        }
+    private boolean hasReadResolve(Class<? extends Serializable> clazz) {
+        return getReadResolve(clazz) != null;
+    }
 
-        if (writeReplace != null) {
-            serializationType |= SerializationType.SERIALIZABLE_WRITE_REPLACE;
-        }
+    private boolean hasWriteReplace(Class<? extends Serializable> clazz) {
+        return getWriteReplace(clazz) != null;
+    }
 
-        if (readResolve != null) {
-            serializationType |= SerializationType.SERIALIZABLE_READ_RESOLVE;
-        }
+    private boolean hasOverrideSerialization(Class<? extends Serializable> clazz) {
+        Method writeObject = getWriteObject(clazz);
+        Method readObject = getReadObject(clazz);
+        Method readObjectNoData = getReadObjectNoData(clazz);
 
-        return new ClassDescriptor(clazz, descriptorId, fields(clazz), serializationType);
+        return writeObject != null && readObject != null && readObjectNoData != null;
     }
 
     /**
@@ -195,7 +200,7 @@ public class ClassDescriptorFactory {
      * @return Class descriptor.
      */
     private ClassDescriptor arbitrary(int descriptorId, Class<?> clazz) {
-        return new ClassDescriptor(clazz, descriptorId, fields(clazz), SerializationType.ARBITRARY);
+        return new ClassDescriptor(clazz, descriptorId, fields(clazz), new Serialization(SerializationType.ARBITRARY));
     }
 
     /**
@@ -231,13 +236,7 @@ public class ClassDescriptorFactory {
     @Nullable
     private static Method getWriteReplace(Class<? extends Serializable> clazz) {
         try {
-            Method writeReplace = clazz.getDeclaredMethod("writeReplace");
-
-            if (!declaresExactExceptions(writeReplace, Set.of(ObjectStreamException.class))) {
-                return null;
-            }
-
-            return writeReplace;
+            return clazz.getDeclaredMethod("writeReplace");
         } catch (NoSuchMethodException e) {
             return null;
         }
@@ -253,13 +252,7 @@ public class ClassDescriptorFactory {
     @Nullable
     private static Method getReadResolve(Class<? extends Serializable> clazz) {
         try {
-            Method readResolve = clazz.getDeclaredMethod("readResolve");
-
-            if (!declaresExactExceptions(readResolve, Set.of(ObjectStreamException.class))) {
-                return null;
-            }
-
-            return readResolve;
+            return clazz.getDeclaredMethod("readResolve");
         } catch (NoSuchMethodException e) {
             return null;
         }
@@ -281,10 +274,6 @@ public class ClassDescriptorFactory {
                 return null;
             }
 
-            if (!declaresExactExceptions(writeObject, Set.of(IOException.class))) {
-                return null;
-            }
-
             return writeObject;
         } catch (NoSuchMethodException e) {
             return null;
@@ -308,10 +297,6 @@ public class ClassDescriptorFactory {
                 return null;
             }
 
-            if (!declaresExactExceptions(writeObject, Set.of(IOException.class, ClassNotFoundException.class))) {
-                return null;
-            }
-
             return writeObject;
         } catch (NoSuchMethodException e) {
             return null;
@@ -334,32 +319,9 @@ public class ClassDescriptorFactory {
                 return null;
             }
 
-            if (!declaresExactExceptions(writeObject, Set.of(ObjectStreamException.class))) {
-                return null;
-            }
-
             return writeObject;
         } catch (NoSuchMethodException e) {
             return null;
         }
     }
-
-    /**
-     * Returns {@code true} if the method's declaration contains throwing only of
-     * specified exceptions.
-     *
-     * @param method Method.
-     * @param exceptions Set of exceptions.
-     * @return If the method throws exceptions.
-     */
-    @SuppressWarnings("BooleanMethodIsAlwaysInverted")
-    private static boolean declaresExactExceptions(Method method, Set<Class<? extends Throwable>> exceptions) {
-        Class<?>[] exceptionTypes = method.getExceptionTypes();
-
-        if (exceptionTypes.length != exceptions.size()) {
-            return false;
-        }
-
-        return Arrays.asList(exceptionTypes).containsAll(exceptions);
-    }
 }
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 92ff68a..43b148c 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
@@ -42,7 +42,7 @@ public class ClassDescriptorFactoryContext {
      * Constructor.
      */
     public ClassDescriptorFactoryContext() {
-        for (DefaultType value : DefaultType.values()) {
+        for (BuiltinType value : BuiltinType.values()) {
             addPredefinedDescriptor(value.clazz(), value.asClassDescriptor());
         }
     }
@@ -104,6 +104,20 @@ public class ClassDescriptorFactoryContext {
     }
 
     /**
+     * Returns a descriptor by its ID or throws an exception if the ID is not known.
+     *
+     * @param descriptorId ID by which to obtain a descriptor
+     * @return descriptor
+     */
+    public ClassDescriptor getRequiredDescriptor(int descriptorId) {
+        ClassDescriptor descriptor = getDescriptor(descriptorId);
+        if (descriptor == null) {
+            throw new IllegalStateException("No descriptor exists with ID=" + descriptorId);
+        }
+        return descriptor;
+    }
+
+    /**
      * Returns {@code true} if there is a descriptor for the id.
      *
      * @param descriptorId Descriptor id.
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
new file mode 100644
index 0000000..0f58c70
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/Serialization.java
@@ -0,0 +1,97 @@
+/*
+ * 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;
+
+/**
+ * Describes how a class is to be serialized.
+ */
+public class Serialization {
+    /** Serialization type. */
+    private final SerializationType type;
+
+    /** Whether a Serializable has writeObject()/readObject()/readObjectNoData() methods. */
+    private final boolean hasSerializationOverride;
+    /** Whether a Serializable/Externalizable has writeReplace() method. */
+    private final boolean hasWriteReplace;
+    /** Whether a Serializable/Externalizable has readResolve() method. */
+    private final boolean hasReadResolve;
+
+    /**
+     * Creates a new Serialization.
+     *
+     * @param type                     type
+     * @param hasSerializationOverride whether a Serializable has writeObject()/readObject()/readObjectNoData() methods
+     * @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 hasWriteReplace, boolean hasReadResolve) {
+        assert type == SerializationType.SERIALIZABLE
+                || (type == SerializationType.EXTERNALIZABLE && !hasSerializationOverride)
+                || (!hasSerializationOverride && !hasWriteReplace && !hasReadResolve);
+
+        this.type = type;
+        this.hasSerializationOverride = hasSerializationOverride;
+        this.hasWriteReplace = hasWriteReplace;
+        this.hasReadResolve = hasReadResolve;
+    }
+
+    /**
+     * Creates a new Serialization with all optional features disabled.
+     *
+     * @param type serialization type
+     */
+    public Serialization(SerializationType type) {
+        this(type, false, false, false);
+    }
+
+    /**
+     * Returns serialization type.
+     *
+     * @return serialization type
+     */
+    public SerializationType type() {
+        return type;
+    }
+
+    /**
+     * Returns whether serialization override (writeObject()/readObject()/readObjectNoData()) is present.
+     *
+     * @return whether serialization override (writeObject()/readObject()/readObjectNoData()) is present
+     */
+    public boolean hasSerializationOverride() {
+        return hasSerializationOverride;
+    }
+
+    /**
+     * Returns whether writeReplace() method is present.
+     *
+     * @return whether writeReplace() method is present
+     */
+    public boolean hasWriteReplace() {
+        return hasWriteReplace;
+    }
+
+    /**
+     * Returns whether readResolve() method is present.
+     *
+     * @return whether readResolve() method is present
+     */
+    public boolean hasReadResolve() {
+        return hasReadResolve;
+    }
+}
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 99b462f..c5cc87d 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
@@ -18,27 +18,25 @@
 package org.apache.ignite.internal.network.serialization;
 
 /**
- * Serialization type flags.
+ * Serialization type.
  */
-public class SerializationType {
-    /** Used for predefined descriptors like primitive (or boxed int). See {@link DefaultType}. */
-    public static final int DEFAULT = 0;
-
+public enum SerializationType {
+    /** Used for predefined descriptors like primitive (or boxed int). See {@link BuiltinType}. */
+    BUILTIN(0),
     /** Type for classes that are neither serializable nor externalizable.  */
-    public static final int ARBITRARY = 1;
-
+    ARBITRARY(1),
     /** Externalizable. */
-    public static final int EXTERNALIZABLE = 1 << 1;
-
-    /** Simple serializable. */
-    public static final int SERIALIZABLE = 1 << 2;
+    EXTERNALIZABLE(2),
+    /** Serializable (but not Externalizable). */
+    SERIALIZABLE(3);
 
-    /** Serializable with readObject/writeObject flag. */
-    public static final int SERIALIZABLE_OVERRIDE = 1 << 3;
+    private final int value;
 
-    /** Serializable with writeReplace flag. */
-    public static final int SERIALIZABLE_WRITE_REPLACE = 1 << 4;
+    SerializationType(int value) {
+        this.value = value;
+    }
 
-    /** Serializable with readResolve flag. */
-    public static final int SERIALIZABLE_READ_RESOLVE = 1 << 5;
+    public int value() {
+        return value;
+    }
 }
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
new file mode 100644
index 0000000..17b8644
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshaller.java
@@ -0,0 +1,181 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.serialization.marshal;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.List;
+import org.apache.ignite.internal.network.serialization.ClassDescriptor;
+import org.apache.ignite.internal.network.serialization.ClassDescriptorFactory;
+import org.apache.ignite.internal.network.serialization.ClassDescriptorFactoryContext;
+
+/**
+ * Default implementation of {@link UserObjectMarshaller}.
+ */
+public class DefaultUserObjectMarshaller implements UserObjectMarshaller {
+    private final ClassDescriptorFactoryContext descriptorRegistry;
+    private final ClassDescriptorFactory descriptorFactory;
+
+    private final SpecialSerializationMethodsCache serializationMethodsCache = new SpecialSerializationMethodsCache();
+
+    public DefaultUserObjectMarshaller(ClassDescriptorFactoryContext descriptorRegistry, ClassDescriptorFactory descriptorFactory) {
+        this.descriptorRegistry = descriptorRegistry;
+        this.descriptorFactory = descriptorFactory;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public MarshalledObject marshal(Object object) throws MarshalException {
+        ClassDescriptor descriptor = getOrCreateDescriptor(object);
+
+        var baos = new ByteArrayOutputStream();
+
+        final List<ClassDescriptor> usedDescriptors;
+        try (var dos = new DataOutputStream(baos)) {
+            writeDescriptorId(descriptor, dos);
+
+            usedDescriptors = writeObject(object, descriptor, dos);
+        } catch (IOException e) {
+            throw new MarshalException("Cannot marshal", e);
+        }
+
+        return new MarshalledObject(baos.toByteArray(), usedDescriptors);
+    }
+
+    private ClassDescriptor getOrCreateDescriptor(Object object) {
+        ClassDescriptor descriptor = descriptorRegistry.getDescriptor(object.getClass());
+        if (descriptor == null) {
+            descriptor = descriptorFactory.create(object.getClass());
+        }
+        return descriptor;
+    }
+
+    private void writeDescriptorId(ClassDescriptor descriptor, DataOutputStream dos) throws IOException {
+        dos.writeInt(descriptor.descriptorId());
+    }
+
+    private List<ClassDescriptor> writeObject(Object object, ClassDescriptor descriptor, DataOutputStream dos)
+            throws IOException, MarshalException {
+        final Object objectToWrite;
+        if ((descriptor.isSerializable() || descriptor.isExternalizable()) && descriptor.hasWriteReplace()) {
+            // TODO: IGNITE-16155 what if non-Externalizable is returned?
+            objectToWrite = applyWriteReplace(object, descriptor);
+        } else {
+            objectToWrite = object;
+        }
+
+        if (descriptor.isExternalizable()) {
+            return writeExternalizable((Externalizable) objectToWrite, descriptor, dos);
+        } else {
+            throw new UnsupportedOperationException("Not supported yet");
+        }
+    }
+
+    private Object applyWriteReplace(Object object, ClassDescriptor descriptor) throws MarshalException {
+        return serializationMethodsCache.methodsFor(descriptor).writeReplace(object);
+
+        // TODO: IGNITE-16155 what if null is returned?
+    }
+
+    private List<ClassDescriptor> writeExternalizable(
+            Externalizable externalizable,
+            ClassDescriptor descriptor,
+            DataOutputStream dos
+    ) throws IOException {
+        byte[] externalizableBytes = externalize(externalizable);
+
+        dos.writeInt(externalizableBytes.length);
+        dos.write(externalizableBytes);
+
+        return List.of(descriptor);
+    }
+
+    private byte[] externalize(Externalizable externalizable) throws IOException {
+        var baos = new ByteArrayOutputStream();
+        try (var oos = new ObjectOutputStream(baos)) {
+            externalizable.writeExternal(oos);
+        }
+
+        return baos.toByteArray();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <T> T unmarshal(byte[] bytes) throws UnmarshalException {
+        try (var dis = new DataInputStream(new ByteArrayInputStream(bytes))) {
+            int descriptorId = readDescriptorId(dis);
+            ClassDescriptor descriptor = descriptorRegistry.getRequiredDescriptor(descriptorId);
+
+            if (descriptor.isExternalizable()) {
+                Object readObject = readExternalizable(descriptor, dis);
+                Object resultObject;
+                if (descriptor.hasReadResolve()) {
+                    resultObject = applyReadResolve(readObject, descriptor);
+                } else {
+                    resultObject = readObject;
+                }
+                @SuppressWarnings("unchecked") T castResult = (T) resultObject;
+                return castResult;
+            } else {
+                throw new UnsupportedOperationException("Not supported yet");
+            }
+        } catch (IOException | ClassNotFoundException e) {
+            throw new UnmarshalException("Cannot unmarshal", e);
+        }
+    }
+
+    private int readDescriptorId(DataInputStream dis) throws IOException {
+        return dis.readInt();
+    }
+
+    private <T extends Externalizable> T readExternalizable(ClassDescriptor descriptor, DataInputStream dis)
+            throws IOException, ClassNotFoundException, UnmarshalException {
+        T object = instantiateObject(descriptor);
+
+        int length = dis.readInt();
+        byte[] bytes = new byte[length];
+        dis.readFully(bytes);
+
+        try (var ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
+            object.readExternal(ois);
+        }
+
+        return object;
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T extends Externalizable> T instantiateObject(ClassDescriptor descriptor) throws UnmarshalException {
+        try {
+            return (T) descriptor.clazz().getConstructor().newInstance();
+        } catch (ReflectiveOperationException e) {
+            throw new UnmarshalException("Cannot instantiate " + descriptor.clazz(), e);
+        }
+    }
+
+    private Object applyReadResolve(Object object, ClassDescriptor descriptor) throws UnmarshalException {
+        return serializationMethodsCache.methodsFor(descriptor).readResolve(object);
+
+        // TODO: IGNITE-16155 what if null is returned?
+    }
+}
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/MarshalException.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/MarshalException.java
new file mode 100644
index 0000000..7406a92
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/MarshalException.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.serialization.marshal;
+
+import org.apache.ignite.lang.IgniteInternalCheckedException;
+
+/**
+ * Thrown when marshalling fails.
+ */
+public class MarshalException extends IgniteInternalCheckedException {
+    public MarshalException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/MarshalledObject.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/MarshalledObject.java
new file mode 100644
index 0000000..96bb887
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/MarshalledObject.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.serialization.marshal;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+import org.apache.ignite.internal.network.serialization.ClassDescriptor;
+
+/**
+ * Represents a marshalled object: the marshalled representation with information about how it was marshalled
+ * (including what descriptors were used when marshalling it).
+ */
+public class MarshalledObject {
+    /** Marshalled object representation. */
+    private final byte[] bytes;
+
+    /** The descriptors that were used while marshalling the object. */
+    private final List<ClassDescriptor> usedDescriptors;
+
+    /**
+     * Creates a new {@link MarshalledObject}.
+     *
+     * @param bytes           marshalled representation bytes
+     * @param usedDescriptors the descriptors that were used to marshal the object
+     */
+    public MarshalledObject(byte[] bytes, List<ClassDescriptor> usedDescriptors) {
+        Objects.requireNonNull(bytes, "bytes is null");
+        Objects.requireNonNull(usedDescriptors, "usedDescriptors is null");
+
+        this.bytes = Arrays.copyOf(bytes, bytes.length);
+        this.usedDescriptors = List.copyOf(usedDescriptors);
+    }
+
+    /**
+     * Returns marshalled object representation.
+     *
+     * @return marshalled object representation
+     */
+    public byte[] bytes() {
+        return Arrays.copyOf(bytes, bytes.length);
+    }
+
+    /**
+     * Returns the descriptors that were used while marshalling the object.
+     *
+     * @return the descriptors that were used while marshalling the object
+     */
+    public List<ClassDescriptor> usedDescriptors() {
+        return usedDescriptors;
+    }
+}
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/ReflectionException.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/ReflectionException.java
new file mode 100644
index 0000000..91be33a
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/ReflectionException.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.serialization.marshal;
+
+import org.apache.ignite.lang.IgniteInternalException;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Thrown when a reflection operation fails.
+ */
+public class ReflectionException extends IgniteInternalException {
+    public ReflectionException(String msg, @Nullable Throwable cause) {
+        super(msg, cause);
+    }
+}
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SpecialSerializationMethods.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SpecialSerializationMethods.java
new file mode 100644
index 0000000..e8dce7c
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SpecialSerializationMethods.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.serialization.marshal;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Method;
+import java.util.Objects;
+import org.apache.ignite.internal.network.serialization.ClassDescriptor;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Encapsulates special serialization methods like writeReplace()/readResolve() for convenient invocation.
+ */
+class SpecialSerializationMethods {
+    /** MethodHandle that can be used to invoke writeReplace() on the target class. */
+    @Nullable
+    private final MethodHandle writeReplaceHandle;
+
+    /** MethodHandle that can be used to invoke readResolve() on the target class. */
+    @Nullable
+    private final MethodHandle readResolveHandle;
+
+    /**
+     * Creates a new instance from the provided descriptor.
+     *
+     * @param descriptor class descriptor on which class to operate
+     */
+    public SpecialSerializationMethods(ClassDescriptor descriptor) {
+        writeReplaceHandle = descriptor.hasWriteReplace() ? writeReplaceHandle(descriptor) : null;
+        readResolveHandle = descriptor.hasReadResolve() ? readResolveHandle(descriptor) : null;
+    }
+
+    private static MethodHandle writeReplaceHandle(ClassDescriptor descriptor) {
+        Method writeReplaceMethod = findWriteReplaceMethod(descriptor);
+
+        try {
+            return MethodHandles.lookup()
+                        .unreflect(writeReplaceMethod)
+                        .asType(MethodType.methodType(Object.class, Object.class));
+        } catch (IllegalAccessException e) {
+            throw new ReflectionException("writeReplace() cannot be unreflected", e);
+        }
+    }
+
+    private static Method findWriteReplaceMethod(ClassDescriptor descriptor) {
+        Method writeReplaceMethod;
+        try {
+            writeReplaceMethod = descriptor.clazz().getDeclaredMethod("writeReplace");
+        } catch (NoSuchMethodException e) {
+            throw new ReflectionException("writeReplace() was not found on " + descriptor.clazz()
+                    + " even though the descriptor says the class has the method", e);
+        }
+
+        writeReplaceMethod.setAccessible(true);
+
+        return writeReplaceMethod;
+    }
+
+    private static MethodHandle readResolveHandle(ClassDescriptor descriptor) {
+        Method readResolveMethod = findReadResolveMethod(descriptor);
+
+        try {
+            return MethodHandles.lookup()
+                    .unreflect(readResolveMethod)
+                    .asType(MethodType.methodType(Object.class, Object.class));
+        } catch (IllegalAccessException e) {
+            throw new ReflectionException("readResolve() cannot be unreflected", e);
+        }
+    }
+
+    @NotNull
+    private static Method findReadResolveMethod(ClassDescriptor descriptor) {
+        Method readResolveMethod;
+        try {
+            readResolveMethod = descriptor.clazz().getDeclaredMethod("readResolve");
+        } catch (NoSuchMethodException e) {
+            throw new ReflectionException("readResolve() was not found on " + descriptor.clazz()
+                    + " even though the descriptor says the class has the method", e);
+        }
+
+        readResolveMethod.setAccessible(true);
+
+        return readResolveMethod;
+    }
+
+    /**
+     * Invokes {@code writeReplace()} on the target object. Should only be used if the target descriptor supports
+     * the method (that is, its serialization type is
+     * {@link org.apache.ignite.internal.network.serialization.SerializationType#SERIALIZABLE} or
+     * {@link org.apache.ignite.internal.network.serialization.SerializationType#EXTERNALIZABLE} and the class
+     * actually has writeReplace() method).
+     * If any of these conditions fail, a {@link NullPointerException} will be thrown.
+     *
+     * @param object target object on which to invoke the method
+     * @return invocation result
+     * @throws MarshalException if the invocation fails
+     */
+    public Object writeReplace(Object object) throws MarshalException {
+        Objects.requireNonNull(writeReplaceHandle);
+
+        try {
+            return writeReplaceHandle.invokeExact(object);
+        } catch (Error e) {
+            throw e;
+        } catch (Throwable e) {
+            throw new MarshalException("writeReplace() invocation failed on " + object, e);
+        }
+    }
+
+    /**
+     * Invokes {@code readResolve()} on the target object. Should only be used if the target descriptor supports
+     * the method (that is, its serialization type is
+     * {@link org.apache.ignite.internal.network.serialization.SerializationType#SERIALIZABLE} or
+     * {@link org.apache.ignite.internal.network.serialization.SerializationType#EXTERNALIZABLE} and the class
+     * actually has readResolve() method).
+     * If any of these conditions fail, a {@link NullPointerException} will be thrown.
+     *
+     * @param object target object on which to invoke the method
+     * @return invocation result
+     * @throws UnmarshalException if the invocation fails
+     */
+    public Object readResolve(Object object) throws UnmarshalException {
+        Objects.requireNonNull(readResolveHandle);
+
+        try {
+            return readResolveHandle.invokeExact(object);
+        } catch (Error e) {
+            throw e;
+        } catch (Throwable e) {
+            throw new UnmarshalException("readResolve() invocation failed on " + object, e);
+        }
+    }
+}
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SpecialSerializationMethodsCache.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SpecialSerializationMethodsCache.java
new file mode 100644
index 0000000..1be3cf3
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/SpecialSerializationMethodsCache.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.serialization.marshal;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import org.apache.ignite.internal.network.serialization.ClassDescriptor;
+
+/**
+ * Caches {@link SpecialSerializationMethods} per class descriptor.
+ */
+class SpecialSerializationMethodsCache {
+    private final ConcurrentMap<Integer, SpecialSerializationMethods> methodsMap = new ConcurrentHashMap<>();
+
+    public SpecialSerializationMethods methodsFor(ClassDescriptor descriptor) {
+        return methodsMap.computeIfAbsent(descriptor.descriptorId(), id -> new SpecialSerializationMethods(descriptor));
+    }
+}
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UnmarshalException.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UnmarshalException.java
new file mode 100644
index 0000000..9e7b624
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UnmarshalException.java
@@ -0,0 +1,29 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.serialization.marshal;
+
+import org.apache.ignite.lang.IgniteInternalCheckedException;
+
+/**
+ * Thrown when unmarshalling fails.
+ */
+public class UnmarshalException extends IgniteInternalCheckedException {
+    public UnmarshalException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UserObjectMarshaller.java b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UserObjectMarshaller.java
new file mode 100644
index 0000000..0b75743
--- /dev/null
+++ b/modules/network/src/main/java/org/apache/ignite/internal/network/serialization/marshal/UserObjectMarshaller.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.serialization.marshal;
+
+/**
+ * Marshals/unmarshals objects in accordance with User Object Serialization.
+ *
+ * @see <a href="https://github.com/gridgain/gridgain-9-ce/blob/iep-67/modules/network/README.md">IEP-67</a>
+ */
+public interface UserObjectMarshaller {
+    /**
+     * Marshals the provided object.
+     *
+     * @param object object to marshal
+     * @return marshalled representation
+     * @throws MarshalException if marshalling fails
+     */
+    MarshalledObject marshal(Object object) throws MarshalException;
+
+    /**
+     * Unmarshals an object.
+     *
+     * @param bytes bytes representing the marshalled object
+     * @param <T> expected type
+     * @return unmarshalled object
+     * @throws UnmarshalException if unmarshalling fails
+     */
+    <T> T unmarshal(byte[] bytes) throws UnmarshalException;
+}
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 0c15e73..ca5ab1c 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
@@ -20,10 +20,8 @@ package org.apache.ignite.internal.network.serialization;
 import static org.apache.ignite.internal.network.serialization.SerializationType.ARBITRARY;
 import static org.apache.ignite.internal.network.serialization.SerializationType.EXTERNALIZABLE;
 import static org.apache.ignite.internal.network.serialization.SerializationType.SERIALIZABLE;
-import static org.apache.ignite.internal.network.serialization.SerializationType.SERIALIZABLE_OVERRIDE;
-import static org.apache.ignite.internal.network.serialization.SerializationType.SERIALIZABLE_READ_RESOLVE;
-import static org.apache.ignite.internal.network.serialization.SerializationType.SERIALIZABLE_WRITE_REPLACE;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
@@ -64,7 +62,7 @@ public class ClassDescriptorFactoryTest {
     public void testSerializable() {
         ClassDescriptor descriptor = factory.create(SerializableClass.class);
 
-        checkSerializable(descriptor.serializationType(), false, false, false);
+        checkSerializable(descriptor.serialization(), false, false, false);
         assertEquals(0, descriptor.fields().size());
     }
 
@@ -89,7 +87,7 @@ public class ClassDescriptorFactoryTest {
     public void testSerializableOverride() {
         ClassDescriptor descriptor = factory.create(SerializableOverride.class);
 
-        checkSerializable(descriptor.serializationType(), true, false, false);
+        checkSerializable(descriptor.serialization(), true, false, false);
         assertEquals(0, descriptor.fields().size());
     }
 
@@ -119,7 +117,7 @@ public class ClassDescriptorFactoryTest {
     public void testSerializableOverrideWriteReplace() {
         ClassDescriptor descriptor = factory.create(SerializableOverrideWriteReplace.class);
 
-        checkSerializable(descriptor.serializationType(), true, true, false);
+        checkSerializable(descriptor.serialization(), true, true, false);
         assertEquals(0, descriptor.fields().size());
     }
 
@@ -149,7 +147,7 @@ public class ClassDescriptorFactoryTest {
     public void testSerializableOverrideReadResolve() {
         ClassDescriptor descriptor = factory.create(SerializableOverrideReadResolve.class);
 
-        checkSerializable(descriptor.serializationType(), true, false, true);
+        checkSerializable(descriptor.serialization(), true, false, true);
         assertEquals(0, descriptor.fields().size());
     }
 
@@ -185,7 +183,7 @@ public class ClassDescriptorFactoryTest {
         ClassDescriptor descriptor = factory.create(
                 SerializableOverrideWriteReplaceReadResolve.class);
 
-        checkSerializable(descriptor.serializationType(), true, true, true);
+        checkSerializable(descriptor.serialization(), true, true, true);
         assertEquals(0, descriptor.fields().size());
     }
 
@@ -202,7 +200,7 @@ public class ClassDescriptorFactoryTest {
     public void testSerializableWriteReplace() {
         ClassDescriptor descriptor = factory.create(SerializableWriteReplace.class);
 
-        checkSerializable(descriptor.serializationType(), false, true, false);
+        checkSerializable(descriptor.serialization(), false, true, false);
         assertEquals(0, descriptor.fields().size());
     }
 
@@ -220,7 +218,7 @@ public class ClassDescriptorFactoryTest {
     public void testSerializableReadResolve() {
         ClassDescriptor descriptor = factory.create(SerializableReadResolve.class);
 
-        checkSerializable(descriptor.serializationType(), false, false, true);
+        checkSerializable(descriptor.serialization(), false, false, true);
         assertEquals(0, descriptor.fields().size());
     }
 
@@ -242,7 +240,7 @@ public class ClassDescriptorFactoryTest {
     public void testSerializableWriteReplaceReadResolve() {
         ClassDescriptor descriptor = factory.create(SerializableWriteReplaceReadResolve.class);
 
-        checkSerializable(descriptor.serializationType(), false, true, true);
+        checkSerializable(descriptor.serialization(), false, true, true);
         assertEquals(0, descriptor.fields().size());
     }
 
@@ -266,7 +264,7 @@ public class ClassDescriptorFactoryTest {
     public void testExternalizable() {
         ClassDescriptor descriptor = factory.create(ExternalizableClass.class);
 
-        checkExternalizable(descriptor.serializationType());
+        checkSimpleExternalizable(descriptor.serialization());
         assertEquals(0, descriptor.fields().size());
     }
 
@@ -277,7 +275,7 @@ public class ClassDescriptorFactoryTest {
     public void testArbitrary() {
         ClassDescriptor descriptor = factory.create(ArbitraryClass.class);
 
-        checkArbitraryType(descriptor.serializationType());
+        checkArbitraryType(descriptor.serialization());
         assertEquals(0, descriptor.fields().size());
     }
 
@@ -328,7 +326,7 @@ public class ClassDescriptorFactoryTest {
         ClassDescriptor arbitraryDescriptor = context.getDescriptor(ArbitraryClass.class);
         assertNotNull(arbitraryDescriptor);
 
-        ClassDescriptor intDescriptor = context.getDescriptor(DefaultType.INT.descriptorId());
+        ClassDescriptor intDescriptor = context.getDescriptor(BuiltinType.INT.descriptorId());
         assertNotNull(intDescriptor);
 
         List<FieldDescriptor> fields = holderDescriptor.fields();
@@ -358,10 +356,10 @@ public class ClassDescriptorFactoryTest {
         ClassDescriptor descriptor = context.getDescriptor(int.class);
 
         assertNotNull(descriptor);
-        checkDefaultType(descriptor.serializationType());
+        checkBuiltInType(descriptor.serializationType());
     }
 
-    private static class SerializableWithWrongSignatures implements Serializable {
+    private static class SerializableWithoutExceptionsOnSpecialMethods implements Serializable {
         private static final long serialVersionUID = 0L;
 
         private void writeObject(ObjectOutputStream out) {
@@ -388,64 +386,59 @@ public class ClassDescriptorFactoryTest {
     }
 
     @Test
-    public void testSerializableWithIncorrectMethodSignatures() {
-        ClassDescriptor descriptor = factory.create(SerializableWithWrongSignatures.class);
+    public void testSerializableWithoutExceptionsOnSpecialMethods() {
+        ClassDescriptor descriptor = factory.create(SerializableWithoutExceptionsOnSpecialMethods.class);
 
         assertNotNull(descriptor);
-        checkSerializable(descriptor.serializationType(), false, false, false);
+        checkSerializable(descriptor.serialization(), true, true, true);
     }
 
     /**
      * Checks that serialization type is {@link SerializationType#ARBITRARY}.
      *
-     * @param type Serialization type.
+     * @param serialization Serialization.
      */
-    private void checkArbitraryType(int type) {
-        assertEquals(ARBITRARY, type & ARBITRARY);
-        assertEquals(0, type & SERIALIZABLE);
-        assertEquals(0, type & SERIALIZABLE_OVERRIDE);
-        assertEquals(0, type & SERIALIZABLE_WRITE_REPLACE);
-        assertEquals(0, type & SERIALIZABLE_READ_RESOLVE);
-        assertEquals(0, type & EXTERNALIZABLE);
+    private void checkArbitraryType(Serialization serialization) {
+        assertEquals(ARBITRARY, serialization.type());
+        assertFalse(serialization.hasSerializationOverride());
+        assertFalse(serialization.hasWriteReplace());
+        assertFalse(serialization.hasReadResolve());
     }
 
     /**
-     * Checks that serialization type is {@link SerializationType#DEFAULT}.
+     * Checks that serialization type is {@link SerializationType#BUILTIN}.
      *
      * @param type Serialization type.
      */
-    private void checkDefaultType(int type) {
-        assertEquals(0, type);
+    private void checkBuiltInType(SerializationType type) {
+        assertEquals(SerializationType.BUILTIN, type);
     }
 
     /**
      * Checks that serialization type is {@link SerializationType#EXTERNALIZABLE}.
      *
-     * @param type Serialization type.
+     * @param serialization Serialization type.
      */
-    private void checkExternalizable(int type) {
-        assertEquals(0, type & ARBITRARY);
-        assertEquals(0, type & SERIALIZABLE);
-        assertEquals(0, type & SERIALIZABLE_OVERRIDE);
-        assertEquals(0, type & SERIALIZABLE_WRITE_REPLACE);
-        assertEquals(0, type & SERIALIZABLE_READ_RESOLVE);
-        assertEquals(EXTERNALIZABLE, type & EXTERNALIZABLE);
+    private void checkSimpleExternalizable(Serialization serialization) {
+        assertEquals(EXTERNALIZABLE, serialization.type());
+        assertFalse(serialization.hasSerializationOverride());
+        assertFalse(serialization.hasWriteReplace());
+        assertFalse(serialization.hasReadResolve());
     }
 
     /**
      * Checks that serialization type is {@link SerializationType#SERIALIZABLE} and has specific flags.
      *
-     * @param type         Serialization type.
-     * @param override     Has readObject/writeObject/readObjectNoData methods.
-     * @param writeReplace Has writeReplace method.
-     * @param readResolve  Has readResolve method.
+     * @param serialization serialization to check.
+     * @param override      Has readObject/writeObject/readObjectNoData methods.
+     * @param writeReplace  Has writeReplace method.
+     * @param readResolve   Has readResolve method.
      */
-    private void checkSerializable(int type, boolean override, boolean writeReplace, boolean readResolve) {
-        assertEquals(0, type & ARBITRARY);
-        assertEquals(SERIALIZABLE, type & SERIALIZABLE);
-        assertEquals(override ? SERIALIZABLE_OVERRIDE : 0, type & SERIALIZABLE_OVERRIDE);
-        assertEquals(writeReplace ? SERIALIZABLE_WRITE_REPLACE : 0, type & SERIALIZABLE_WRITE_REPLACE);
-        assertEquals(readResolve ? SERIALIZABLE_READ_RESOLVE : 0, type & SERIALIZABLE_READ_RESOLVE);
-        assertEquals(0, type & EXTERNALIZABLE);
+    private void checkSerializable(Serialization serialization, boolean override, boolean writeReplace, boolean readResolve) {
+        assertEquals(SERIALIZABLE, serialization.type());
+
+        assertEquals(override, serialization.hasSerializationOverride());
+        assertEquals(writeReplace, serialization.hasWriteReplace());
+        assertEquals(readResolve, serialization.hasReadResolve());
     }
 }
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 a9c5fb5..a663562 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,51 +17,51 @@
 
 package org.apache.ignite.internal.network.serialization;
 
-import static org.apache.ignite.internal.network.serialization.DefaultType.ARRAY_LIST;
-import static org.apache.ignite.internal.network.serialization.DefaultType.BIT_SET;
-import static org.apache.ignite.internal.network.serialization.DefaultType.BOOLEAN;
-import static org.apache.ignite.internal.network.serialization.DefaultType.BOOLEAN_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.BOOLEAN_BOXED;
-import static org.apache.ignite.internal.network.serialization.DefaultType.BYTE;
-import static org.apache.ignite.internal.network.serialization.DefaultType.BYTE_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.BYTE_BOXED;
-import static org.apache.ignite.internal.network.serialization.DefaultType.CHAR;
-import static org.apache.ignite.internal.network.serialization.DefaultType.CHAR_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.CHAR_BOXED;
-import static org.apache.ignite.internal.network.serialization.DefaultType.DATE;
-import static org.apache.ignite.internal.network.serialization.DefaultType.DECIMAL;
-import static org.apache.ignite.internal.network.serialization.DefaultType.DECIMAL_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.DOUBLE;
-import static org.apache.ignite.internal.network.serialization.DefaultType.DOUBLE_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.DOUBLE_BOXED;
-import static org.apache.ignite.internal.network.serialization.DefaultType.ENUM;
-import static org.apache.ignite.internal.network.serialization.DefaultType.ENUM_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.FLOAT;
-import static org.apache.ignite.internal.network.serialization.DefaultType.FLOAT_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.FLOAT_BOXED;
-import static org.apache.ignite.internal.network.serialization.DefaultType.HASH_MAP;
-import static org.apache.ignite.internal.network.serialization.DefaultType.HASH_SET;
-import static org.apache.ignite.internal.network.serialization.DefaultType.IGNITE_UUID;
-import static org.apache.ignite.internal.network.serialization.DefaultType.INT;
-import static org.apache.ignite.internal.network.serialization.DefaultType.INT_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.INT_BOXED;
-import static org.apache.ignite.internal.network.serialization.DefaultType.LINKED_HASH_MAP;
-import static org.apache.ignite.internal.network.serialization.DefaultType.LINKED_HASH_SET;
-import static org.apache.ignite.internal.network.serialization.DefaultType.LINKED_LIST;
-import static org.apache.ignite.internal.network.serialization.DefaultType.LONG;
-import static org.apache.ignite.internal.network.serialization.DefaultType.LONG_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.LONG_BOXED;
-import static org.apache.ignite.internal.network.serialization.DefaultType.NULL;
-import static org.apache.ignite.internal.network.serialization.DefaultType.OBJECT;
-import static org.apache.ignite.internal.network.serialization.DefaultType.OBJECT_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.SHORT;
-import static org.apache.ignite.internal.network.serialization.DefaultType.SHORT_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.SHORT_BOXED;
-import static org.apache.ignite.internal.network.serialization.DefaultType.SINGLETON_LIST;
-import static org.apache.ignite.internal.network.serialization.DefaultType.STRING;
-import static org.apache.ignite.internal.network.serialization.DefaultType.STRING_ARRAY;
-import static org.apache.ignite.internal.network.serialization.DefaultType.UUID;
-import static org.apache.ignite.internal.network.serialization.DefaultType.VOID;
+import static org.apache.ignite.internal.network.serialization.BuiltinType.ARRAY_LIST;
+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;
+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.VOID;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
 import org.junit.jupiter.api.Test;
diff --git a/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerTest.java b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerTest.java
new file mode 100644
index 0000000..f773ed5
--- /dev/null
+++ b/modules/network/src/test/java/org/apache/ignite/internal/network/serialization/marshal/DefaultUserObjectMarshallerTest.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.network.serialization.marshal;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+import java.util.List;
+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.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link DefaultUserObjectMarshaller}.
+ */
+class DefaultUserObjectMarshallerTest {
+    private final ClassDescriptorFactoryContext descriptorRegistry = new ClassDescriptorFactoryContext();
+    private final ClassDescriptorFactory descriptorFactory = new ClassDescriptorFactory(descriptorRegistry);
+
+    private final DefaultUserObjectMarshaller marshaller = new DefaultUserObjectMarshaller(descriptorRegistry, descriptorFactory);
+
+    private static final int WRITE_REPLACE_INCREMENT = 1_000_000;
+    private static final int READ_RESOLVE_INCREMENT = 1_000;
+
+    @Test
+    void usesExactlyOneDescriptorWhenMarshallingExternalizable() throws Exception {
+        MarshalledObject marshalled = marshaller.marshal(new SimpleExternalizable(42));
+
+        ClassDescriptor expectedDescriptor = descriptorRegistry.getDescriptor(SimpleExternalizable.class);
+        assertThat(expectedDescriptor, is(notNullValue()));
+        assertThat(marshalled.usedDescriptors(), is(equalTo(List.of(expectedDescriptor))));
+    }
+
+    @Test
+    void marshalsAndUnmarshalsExternalizable() throws Exception {
+        MarshalledObject marshalled = marshaller.marshal(new SimpleExternalizable(42));
+
+        SimpleExternalizable unmarshalled = marshaller.unmarshal(marshalled.bytes());
+
+        assertThat(unmarshalled, is(notNullValue()));
+        assertThat(unmarshalled.intValue, is(42));
+    }
+
+    @Test
+    void appliesWriteReplaceOnExternalizable() throws Exception {
+        MarshalledObject marshalled = marshaller.marshal(new ExternalizableWithWriteReplace(42));
+
+        SimpleExternalizable unmarshalled = marshaller.unmarshal(marshalled.bytes());
+
+        assertThat(unmarshalled.intValue, is(equalTo(42 + WRITE_REPLACE_INCREMENT)));
+    }
+
+    @Test
+    void appliesReadResolveOnExternalizable() throws Exception {
+        MarshalledObject marshalled = marshaller.marshal(new ExternalizableWithReadResolve(42));
+
+        SimpleExternalizable unmarshalled = marshaller.unmarshal(marshalled.bytes());
+
+        assertThat(unmarshalled.intValue, is(equalTo(42 + READ_RESOLVE_INCREMENT)));
+    }
+
+    @Test
+    void appliesBothWriteReplaceAndReadResolveOnExternalizable() throws Exception {
+        MarshalledObject marshalled = marshaller.marshal(new ExternalizableWithWriteReplaceReadResolve(42));
+
+        SimpleExternalizable unmarshalled = marshaller.unmarshal(marshalled.bytes());
+
+        assertThat(unmarshalled.intValue, is(equalTo(42 + WRITE_REPLACE_INCREMENT + READ_RESOLVE_INCREMENT)));
+    }
+
+    /**
+     * An {@link Externalizable} that does not have {@code writeReplace()}/{@code readResolve()} methods.
+     */
+    private static class SimpleExternalizable implements Externalizable {
+        int intValue;
+
+        public SimpleExternalizable() {
+        }
+
+        public SimpleExternalizable(int intValue) {
+            this.intValue = intValue;
+        }
+
+        @Override
+        public void writeExternal(ObjectOutput out) throws IOException {
+            out.writeInt(-intValue);
+        }
+
+        @Override
+        public void readExternal(ObjectInput in) throws IOException {
+            intValue = -in.readInt();
+        }
+    }
+
+    private static class ExternalizableWithWriteReplace extends SimpleExternalizable {
+        public ExternalizableWithWriteReplace() {
+        }
+
+        public ExternalizableWithWriteReplace(int intValue) {
+            super(intValue);
+        }
+
+        private Object writeReplace() {
+            return new ExternalizableWithWriteReplaceReadResolve(intValue + 1_000_000);
+        }
+    }
+
+    private static class ExternalizableWithReadResolve extends SimpleExternalizable {
+        public ExternalizableWithReadResolve() {
+        }
+
+        public ExternalizableWithReadResolve(int intValue) {
+            super(intValue);
+        }
+
+        private Object readResolve() {
+            return new ExternalizableWithWriteReplaceReadResolve(intValue + 1_000);
+        }
+    }
+
+    private static class ExternalizableWithWriteReplaceReadResolve extends SimpleExternalizable {
+        public ExternalizableWithWriteReplaceReadResolve() {
+        }
+
+        public ExternalizableWithWriteReplaceReadResolve(int intValue) {
+            super(intValue);
+        }
+
+        private Object writeReplace() {
+            return new ExternalizableWithWriteReplaceReadResolve(intValue + 1_000_000);
+        }
+
+        private Object readResolve() {
+            return new ExternalizableWithWriteReplaceReadResolve(intValue + 1_000);
+        }
+    }
+}