You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by am...@apache.org on 2021/11/17 15:29:39 UTC

[ignite-3] branch ignite-15784 created (now 6bf4e2b)

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

amashenkov pushed a change to branch ignite-15784
in repository https://gitbox.apache.org/repos/asf/ignite-3.git.


      at 6bf4e2b  Add mapped types validation. Fix single column mapping. Support POJO fields in mapper. Add tests.

This branch includes the following new commits:

     new 6bf4e2b  Add mapped types validation. Fix single column mapping. Support POJO fields in mapper. Add tests.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


[ignite-3] 01/01: Add mapped types validation. Fix single column mapping. Support POJO fields in mapper. Add tests.

Posted by am...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 6bf4e2b152eb265ac56a985288e61ea676ad828f
Author: Andrew Mashenkov <an...@gmail.com>
AuthorDate: Tue Nov 16 14:26:09 2021 +0300

    Add mapped types validation.
    Fix single column mapping.
    Support POJO fields in mapper.
    Add tests.
    
    Signed-off-by: Andrew Mashenkov <an...@gmail.com>
---
 .../ignite/table/mapper/DefaultColumnMapper.java   |  12 +-
 .../apache/ignite/table/mapper/IdentityMapper.java |  28 +-
 .../org/apache/ignite/table/mapper/Mapper.java     |  92 ++++-
 .../apache/ignite/table/mapper/MapperBuilder.java  |  60 +--
 ...ltColumnMapper.java => SingleColumnMapper.java} |  42 +-
 .../internal/schema/marshaller/BinaryMode.java     |   5 +-
 .../schema/marshaller/MarshallerFactory.java       |   8 +-
 .../internal/schema/marshaller/MarshallerUtil.java |   9 +-
 .../marshaller/asm/AsmSerializerGenerator.java     |   2 +-
 .../marshaller/reflection/FieldAccessor.java       | 437 +++++++++++++--------
 .../schema/marshaller/reflection/Marshaller.java   |  90 +++--
 .../schema/marshaller/KvMarshallerTest.java        | 423 +++++++++++++-------
 .../internal/schema/marshaller/MapperTest.java     |  68 +++-
 .../schema/testobjects/TestOuterObject.java        |  33 ++
 .../org/apache/ignite/internal/table/Example.java  | 103 +++--
 15 files changed, 931 insertions(+), 481 deletions(-)

diff --git a/modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java b/modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java
index e3efc13..f556b81 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java
@@ -21,7 +21,7 @@ import java.util.Map;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Simple mapper implementation that map a column to a class field.
+ * Mapper implementation which maps object fields to the columns by their names.
  *
  * @param <T> Target type.
  */
@@ -47,9 +47,15 @@ class DefaultColumnMapper<T> implements Mapper<T> {
     @Override public Class<T> targetType() {
         return targetType;
     }
-    
+
+    /** {@inheritDoc} */
+    @Override
+    public String mappedColumn() {
+        return null;
+    }
+
     /** {@inheritDoc} */
-    @Override public String columnToField(@NotNull String columnName) {
+    @Override public String mappedField(@NotNull String columnName) {
         return mapping.get(columnName);
     }
 }
diff --git a/modules/api/src/main/java/org/apache/ignite/table/mapper/IdentityMapper.java b/modules/api/src/main/java/org/apache/ignite/table/mapper/IdentityMapper.java
index 7e8d9e2..0f5c458 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/mapper/IdentityMapper.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/mapper/IdentityMapper.java
@@ -23,17 +23,17 @@ import java.util.Set;
 import org.jetbrains.annotations.NotNull;
 
 /**
- * Trivial mapper implementation that maps a column to a field with the same name.
+ * Simple mapper implementation which maps POJO fields to the columns with the same name.
  *
  * @param <T> Target type.
  */
 class IdentityMapper<T> implements Mapper<T> {
     /** Target type. */
     private final Class<T> targetType;
-    
+
     /** Class field names. */
     private final Set<String> fieldsNames;
-    
+
     /**
      * Creates a mapper for given class.
      *
@@ -41,23 +41,33 @@ class IdentityMapper<T> implements Mapper<T> {
      */
     IdentityMapper(Class<T> targetType) {
         this.targetType = targetType;
-        
+
+        //TODO: process inherited fields.
         Field[] fields = targetType.getDeclaredFields();
         fieldsNames = new HashSet<>(fields.length);
-        
+
         for (int i = 0; i < fields.length; i++) {
+            //TODO Filter out 'static' fields.
             //TODO IGNITE-15787 Filter out 'transient' fields.
             fieldsNames.add(fields[i].getName());
         }
     }
-    
+
     /** {@inheritDoc} */
-    @Override public Class<T> targetType() {
+    @Override
+    public Class<T> targetType() {
         return targetType;
     }
-    
+
+    /** {@inheritDoc} */
+    @Override
+    public String mappedColumn() {
+        return null;
+    }
+
     /** {@inheritDoc} */
-    @Override public String columnToField(@NotNull String columnName) {
+    @Override
+    public String mappedField(@NotNull String columnName) {
         return fieldsNames.contains(columnName) ? columnName : null;
     }
 }
diff --git a/modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java b/modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
index 5bdb4ed..ba5a027 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/mapper/Mapper.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.table.mapper;
 
+import java.lang.reflect.Modifier;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -27,50 +28,101 @@ import org.jetbrains.annotations.Nullable;
  */
 public interface Mapper<T> {
     /**
-     * Creates a mapper for a class.
+     * Creates a mapper for a case when object individual fields map to the column with the same names.
      *
-     * @param cls Key class.
-     * @param <K> Key type.
+     * @param cls Target object class.
      * @return Mapper.
      */
-    static <K> Mapper<K> of(Class<K> cls) {
+    static <O> Mapper<O> of(Class<O> cls) {
         return identity(cls);
     }
-    
+
+    /**
+     * Creates a mapper for a simple case when a whole object maps to a single column.
+     *
+     * @param columnName Column name.
+     * @param cls        Target object class.
+     * @return Single column mapper.
+     */
+    static <O> Mapper<O> of(String columnName, Class<O> cls) {
+        return new SingleColumnMapper<>(ensureValidKind(cls), columnName);
+    }
+
     /**
-     * Creates a mapper builder for a class.
+     * Creates a mapper builder for objects of given class.
      *
-     * @param cls Value class.
-     * @param <V> Value type.
+     * @param cls Target object class.
      * @return Mapper builder.
+     * @throws IllegalArgumentException If class is of unsupported kind. E.g. inner, anonymous or local.
      */
-    static <V> MapperBuilder<V> builderFor(Class<V> cls) {
+    static <O> MapperBuilder<O> builderFor(Class<O> cls) {
+        ensureDefaultConsturctor(ensureValidKind(cls));
+
         return new MapperBuilder<>(cls);
     }
-    
+
     /**
      * Creates identity mapper which is used for simple types that have native support or objects with field names that match column names.
      *
-     * @param targetClass Target type class.
+     * @param cls Target type class.
      * @param <T>         Target type.
      * @return Mapper.
      */
-    static <T> Mapper<T> identity(Class<T> targetClass) {
-        return new IdentityMapper<T>(targetClass);
+    static <T> Mapper<T> identity(Class<T> cls) {
+        return new IdentityMapper<T>(ensureValidKind(cls));
+    }
+
+    /**
+     * Ensures class is of the supported kind.
+     *
+     * @param cls Class to validate.
+     * @return {@code cls} if it is valid.
+     * @throws IllegalArgumentException If {@code cls} is invalid and can't be used in mapping.
+     */
+    private static <T> Class<T> ensureValidKind(Class<T> cls) {
+        if (cls.isAnonymousClass() || cls.isLocalClass() || cls.isSynthetic() || cls.isPrimitive() ||
+                (cls.isMemberClass() && !Modifier.isStatic(cls.getModifiers()))) {
+            throw new IllegalArgumentException("Class is of unsupported kind.");
+        }
+
+        return cls;
     }
-    
+
     /**
-     * Return mapped type.
+     * Ensures class has default constructor.
      *
-     * @return Mapped type.
+     * @param cls Class to validate.
+     * @throws IllegalArgumentException If {@code cls} can't be used in mapping.
+     */
+    static <O> void ensureDefaultConsturctor(Class<O> cls) {
+        try {
+            cls.getDeclaredConstructor(new Class[0]);
+        } catch (NoSuchMethodException e) {
+            throw new IllegalArgumentException("Class must have default constructor.");
+        }
+    }
+
+    /**
+     * Returns a type which objects (or their fields) are mapped with the columns.
+     *
+     * @return Mapper target type.
      */
     Class<T> targetType();
-    
+
+    /**
+     * Returns a column name if the whole object is mapped to the single column, otherwise, returns {@code null} and individual column
+     * mapping (see {@link #mappedField(String)) should be used}.
+     *
+     * @return Column name that a whole object is mapped to, or {@code null}.
+     */
+    String mappedColumn();
+
     /**
-     * Maps a column name to a field name.
+     * Return a field name for given column name when POJO individual fields are mapped to columns, otherwise fails.
      *
      * @param columnName Column name.
      * @return Field name or {@code null} if no field mapped to a column.
+     * @throws IllegalStateException If a whole object is mapped to a single column.
      */
-    @Nullable String columnToField(@NotNull String columnName);
-}
+    @Nullable String mappedField(@NotNull String columnName);
+}
\ No newline at end of file
diff --git a/modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java b/modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
index e1357e2..a562190 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/mapper/MapperBuilder.java
@@ -17,6 +17,7 @@
 
 package org.apache.ignite.table.mapper;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Map;
@@ -33,10 +34,14 @@ import org.jetbrains.annotations.NotNull;
 public final class MapperBuilder<T> {
     /** Target type. */
     private Class<T> targetType;
-    
+
     /** Column-to-field name mapping. */
     private Map<String, String> columnToFields;
-    
+
+    private String rawPojoColumnName;
+
+    private boolean isStale;
+
     /**
      * Creates a mapper builder for a type.
      *
@@ -44,10 +49,10 @@ public final class MapperBuilder<T> {
      */
     MapperBuilder(@NotNull Class<T> targetType) {
         this.targetType = targetType;
-        
+
         columnToFields = new HashMap<>(targetType.getDeclaredFields().length);
     }
-    
+
     /**
      * Add mapping for a field to a column.
      *
@@ -55,20 +60,18 @@ public final class MapperBuilder<T> {
      * @param columnName Column name.
      * @return {@code this} for chaining.
      * @throws IllegalArgumentException if a column was already mapped to some field.
-     * @throws IllegalStateException if tries to reuse the builder after a mapping has been built.
+     * @throws IllegalStateException    if tries to reuse the builder after a mapping has been built.
      */
     public MapperBuilder<T> map(@NotNull String fieldName, @NotNull String columnName) {
-        if (columnToFields == null) {
+        if (isStale) {
             throw new IllegalStateException("Mapper builder can't be reused.");
-        }
-        
-        if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
+        } else if (columnToFields.put(Objects.requireNonNull(columnName), Objects.requireNonNull(fieldName)) != null) {
             throw new IllegalArgumentException("Mapping for a column already exists: " + columnName);
         }
-        
+
         return this;
     }
-    
+
     /**
      * Map a field to a type of given class.
      *
@@ -79,7 +82,7 @@ public final class MapperBuilder<T> {
     public MapperBuilder<T> map(@NotNull String fieldName, Class<?> targetClass) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-    
+
     /**
      * Adds a functional mapping for a field, the result depends on function call for every particular row.
      *
@@ -90,17 +93,26 @@ public final class MapperBuilder<T> {
     public MapperBuilder<T> map(@NotNull String fieldName, Function<Tuple, Object> mappingFunction) {
         throw new UnsupportedOperationException("Not implemented yet.");
     }
-    
+
     /**
-     * Sets a target class to deserialize to.
+     * Map an object to a binary column.
      *
-     * @param targetClass Target class.
+     * @param columnName Column name.
      * @return {@code this} for chaining.
      */
-    public MapperBuilder<T> deserializeTo(@NotNull Class<?> targetClass) {
-        throw new UnsupportedOperationException("Not implemented yet.");
+    public MapperBuilder<T> toBinaryColumn(String columnName) {
+        if (isStale) {
+            throw new IllegalStateException("Mapper builder can't be reused.");
+        } else if (!columnToFields.isEmpty()) {
+            throw new IllegalStateException(
+                    "Can't map object to a binary column because the mapping for it`s individual fields is already defined.");
+        }
+
+        columnToFields = null;
+
+        return this;
     }
-    
+
     /**
      * Builds mapper.
      *
@@ -108,22 +120,24 @@ public final class MapperBuilder<T> {
      * @throws IllegalStateException if nothing were mapped or more than one column were mapped to the same field.
      */
     public Mapper<T> build() {
+        isStale = true;
+
         if (columnToFields.isEmpty()) {
             throw new IllegalStateException("Empty mapping isn't allowed.");
         }
-        
+
         Map<String, String> mapping = this.columnToFields;
-        
+
         this.columnToFields = null;
-        
+
         HashSet<String> fields = new HashSet<>(mapping.size());
-        
+
         for (String f : mapping.values()) {
             if (!fields.add(f)) {
                 throw new IllegalStateException("More than one column is mapped to the field: field=" + f);
             }
         }
-        
+
         return new DefaultColumnMapper<>(targetType, mapping);
     }
 }
diff --git a/modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java b/modules/api/src/main/java/org/apache/ignite/table/mapper/SingleColumnMapper.java
similarity index 61%
copy from modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java
copy to modules/api/src/main/java/org/apache/ignite/table/mapper/SingleColumnMapper.java
index e3efc13..9ba1ab4 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/mapper/DefaultColumnMapper.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/mapper/SingleColumnMapper.java
@@ -17,39 +17,41 @@
 
 package org.apache.ignite.table.mapper;
 
-import java.util.Map;
 import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
 
 /**
- * Simple mapper implementation that map a column to a class field.
+ * Simple mapper implementation that maps a whole object (of the target type) to a single column.
  *
  * @param <T> Target type.
  */
-class DefaultColumnMapper<T> implements Mapper<T> {
+class SingleColumnMapper<T> implements Mapper<T> {
     /** Target type. */
     private final Class<T> targetType;
-    
-    /** Column-to-field name mapping. */
-    private final Map<String, String> mapping;
-    
-    /**
-     * Creates a mapper for given type.
-     *
-     * @param targetType Target type.
-     * @param mapping Column-to-field name mapping.
-     */
-    DefaultColumnMapper(Class<T> targetType, Map<String, String> mapping) {
+
+    /** Column name. */
+    private final String mappedColumn;
+
+    SingleColumnMapper(Class<T> targetType, @NotNull String mappedColumn) {
         this.targetType = targetType;
-        this.mapping = mapping;
+        this.mappedColumn = mappedColumn;
     }
-    
+
     /** {@inheritDoc} */
-    @Override public Class<T> targetType() {
+    @Override
+    public Class<T> targetType() {
         return targetType;
     }
-    
+
+    /** {@inheritDoc} */
+    @Override
+    public String mappedColumn() {
+        return mappedColumn;
+    }
+
     /** {@inheritDoc} */
-    @Override public String columnToField(@NotNull String columnName) {
-        return mapping.get(columnName);
+    @Override
+    public @Nullable String mappedField(@NotNull String columnName) {
+        throw new UnsupportedOperationException("Not intended for individual fields mapping.");
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/BinaryMode.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/BinaryMode.java
index 6f73d2c..27d40c9 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/BinaryMode.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/BinaryMode.java
@@ -87,7 +87,10 @@ public enum BinaryMode {
     DATETIME(NativeTypeSpec.DATETIME),
 
     /** Timestamp. */
-    TIMESTAMP(NativeTypeSpec.TIMESTAMP);
+    TIMESTAMP(NativeTypeSpec.TIMESTAMP),
+
+    /** User object. */
+    POJO(NativeTypeSpec.BYTES);
 
     /** Native type spec. */
     private final NativeTypeSpec typeSpec;
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerFactory.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerFactory.java
index 3c8858e..208074c 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerFactory.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerFactory.java
@@ -42,11 +42,11 @@ public interface MarshallerFactory {
      * @param keyClass Key type.
      * @param valueClass Value type.
      * @return Key-value marshaller.
-     * @see Mapper#identity(Class)
+     * @see Mapper#of(Class)
      */
     default <K, V> KvMarshaller<K, V> create(SchemaDescriptor schema, Class<K> keyClass,
             Class<V> valueClass) {
-        return create(schema, Mapper.identity(keyClass), Mapper.identity(valueClass));
+        return create(schema, Mapper.of(keyClass), Mapper.of(valueClass));
     }
     
     /**
@@ -64,9 +64,9 @@ public interface MarshallerFactory {
      * @param schema      Schema descriptor.
      * @param recordClass Record type.
      * @return Record marshaller.
-     * @see Mapper#identity(Class)
+     * @see Mapper#of(Class)
      */
     default <R> RecordMarshaller<R> create(SchemaDescriptor schema, Class<R> recordClass) {
-        return create(schema, Mapper.identity(recordClass));
+        return create(schema, Mapper.of(recordClass));
     }
 }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtil.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtil.java
index 08c0e52..4295fdf 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtil.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/MarshallerUtil.java
@@ -45,8 +45,9 @@ public final class MarshallerUtil {
     public static int getValueSize(Object val, NativeType type) throws InvalidTypeException {
         switch (type.spec()) {
             case BYTES:
-                return ((byte[]) val).length;
-            
+                // Return zero for pojos as they are not serialized yet.
+               return (val instanceof byte[]) ? ((byte[]) val).length : 0;
+
             case STRING:
                 // Overestimating size here prevents from later unwanted row buffer expanding.
                 return ((CharSequence) val).length() << 1;
@@ -118,7 +119,7 @@ public final class MarshallerUtil {
             return BinaryMode.DECIMAL;
         }
         
-        return null;
+        return BinaryMode.POJO;
     }
     
     /**
@@ -129,7 +130,7 @@ public final class MarshallerUtil {
      * @return Object factory.
      */
     public static <T> ObjectFactory<T> factoryForClass(Class<T> targetCls) {
-        if (mode(targetCls) == null) {
+        if (mode(targetCls) == BinaryMode.POJO) {
             return new ObjectFactory<>(targetCls);
         } else {
             return null;
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/asm/AsmSerializerGenerator.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/asm/AsmSerializerGenerator.java
index a51e02f..9c381fc 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/asm/AsmSerializerGenerator.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/asm/AsmSerializerGenerator.java
@@ -184,7 +184,7 @@ public class AsmSerializerGenerator implements SerializerFactory {
     ) {
         final BinaryMode mode = MarshallerUtil.mode(cls);
     
-        if (mode == null) {
+        if (mode == BinaryMode.POJO) {
             return new ObjectMarshallerCodeGenerator(columns, cls, firstColIdx);
         } else {
             return new IdentityMarshallerCodeGenerator(cls, ColumnAccessCodeGenerator.createAccessor(mode, firstColIdx));
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/FieldAccessor.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/FieldAccessor.java
index 54387ba..b9742e3 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/FieldAccessor.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/FieldAccessor.java
@@ -17,6 +17,11 @@
 
 package org.apache.ignite.internal.schema.marshaller.reflection;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
 import java.lang.invoke.MethodHandles;
 import java.lang.invoke.VarHandle;
 import java.lang.reflect.Field;
@@ -43,21 +48,21 @@ import org.apache.ignite.internal.schema.row.RowAssembler;
 abstract class FieldAccessor {
     /** VarHandle. */
     protected final VarHandle varHandle;
-    
+
     /** Mode. */
     protected final BinaryMode mode;
-    
+
     /**
      * Mapped column position in the schema.
      *
      * <p>NODE: Do not mix up with column index in {@link Columns} container.
      */
     protected final int colIdx;
-    
+
     static FieldAccessor noopAccessor(Column col) {
         return new UnmappedFieldAccessor(col);
     }
-    
+
     /**
      * Create accessor for the field.
      *
@@ -70,37 +75,37 @@ abstract class FieldAccessor {
     static FieldAccessor create(Class<?> type, String fldName, Column col, int colIdx) {
         try {
             final Field field = type.getDeclaredField(fldName);
-            
+
             if (field.getType().isPrimitive() && col.nullable()) {
                 throw new IllegalArgumentException("Failed to map non-nullable field to nullable column [name=" + field.getName() + ']');
             }
-            
+
             BinaryMode mode = MarshallerUtil.mode(field.getType());
             final MethodHandles.Lookup lookup = MethodHandles.privateLookupIn(type, MethodHandles.lookup());
-            
+
             VarHandle varHandle = lookup.unreflectVarHandle(field);
-            
+
             assert mode != null : "Invalid mode for type: " + field.getType();
-            
+
             switch (mode) {
                 case P_BYTE:
                     return new BytePrimitiveAccessor(varHandle, colIdx);
-                
+
                 case P_SHORT:
                     return new ShortPrimitiveAccessor(varHandle, colIdx);
-                
+
                 case P_INT:
                     return new IntPrimitiveAccessor(varHandle, colIdx);
-                
+
                 case P_LONG:
                     return new LongPrimitiveAccessor(varHandle, colIdx);
-                
+
                 case P_FLOAT:
                     return new FloatPrimitiveAccessor(varHandle, colIdx);
-                
+
                 case P_DOUBLE:
                     return new DoublePrimitiveAccessor(varHandle, colIdx);
-                
+
                 case BYTE:
                 case SHORT:
                 case INT:
@@ -118,17 +123,19 @@ abstract class FieldAccessor {
                 case DATETIME:
                 case TIMESTAMP:
                     return new ReferenceFieldAccessor(varHandle, colIdx, mode);
-                
+
+                case POJO:
+                    return new PojoFieldAccessor(varHandle, colIdx);
+
                 default:
-                    assert false : "Invalid mode " + mode;
+                    throw new IllegalArgumentException("Failed to create accessor for field [name=" + field.getName() + ']');
             }
-            
-            throw new IllegalArgumentException("Failed to create accessor for field [name=" + field.getName() + ']');
+
         } catch (NoSuchFieldException | SecurityException | IllegalAccessException ex) {
             throw new IllegalArgumentException(ex);
         }
     }
-    
+
     /**
      * Create accessor for the field.
      *
@@ -147,7 +154,7 @@ abstract class FieldAccessor {
             case P_FLOAT:
             case P_DOUBLE:
                 throw new IllegalArgumentException("Primitive key/value types are not possible by API contract.");
-            
+
             case BYTE:
             case SHORT:
             case INT:
@@ -165,14 +172,15 @@ abstract class FieldAccessor {
             case DATETIME:
             case TIMESTAMP:
                 return new IdentityAccessor(colIdx, mode);
-            
+
+            case POJO:
+                return new PojoIdentityAccessor(colIdx);
+
             default:
-                assert false : "Invalid mode " + mode;
+                throw new IllegalArgumentException("Failed to create accessor for column [name=" + col.name() + ']');
         }
-        
-        throw new IllegalArgumentException("Failed to create accessor for column [name=" + col.name() + ']');
     }
-    
+
     /**
      * Reads value object from row.
      *
@@ -184,97 +192,97 @@ abstract class FieldAccessor {
     private static Object readRefValue(Row reader, int colIdx, BinaryMode mode) {
         assert reader != null;
         assert colIdx >= 0;
-        
+
         Object val = null;
-        
+
         switch (mode) {
             case BYTE:
                 val = reader.byteValueBoxed(colIdx);
-                
+
                 break;
-            
+
             case SHORT:
                 val = reader.shortValueBoxed(colIdx);
-                
+
                 break;
-            
+
             case INT:
                 val = reader.intValueBoxed(colIdx);
-                
+
                 break;
-            
+
             case LONG:
                 val = reader.longValueBoxed(colIdx);
-                
+
                 break;
-            
+
             case FLOAT:
                 val = reader.floatValueBoxed(colIdx);
-                
+
                 break;
-            
+
             case DOUBLE:
                 val = reader.doubleValueBoxed(colIdx);
-                
+
                 break;
-            
+
             case STRING:
                 val = reader.stringValue(colIdx);
-                
+
                 break;
-            
+
             case UUID:
                 val = reader.uuidValue(colIdx);
-                
+
                 break;
-            
+
             case BYTE_ARR:
                 val = reader.bytesValue(colIdx);
-                
+
                 break;
-            
+
             case BITSET:
                 val = reader.bitmaskValue(colIdx);
-                
+
                 break;
-            
+
             case NUMBER:
                 val = reader.numberValue(colIdx);
-                
+
                 break;
-            
+
             case DECIMAL:
                 val = reader.decimalValue(colIdx);
-                
+
                 break;
-            
+
             case DATE:
                 val = reader.dateValue(colIdx);
-                
+
                 break;
-            
+
             case TIME:
                 val = reader.timeValue(colIdx);
-                
+
                 break;
-            
+
             case TIMESTAMP:
                 val = reader.timestampValue(colIdx);
-                
+
                 break;
-            
+
             case DATETIME:
                 val = reader.dateTimeValue(colIdx);
-                
+
                 break;
-            
+
             default:
-                assert false : "Invalid mode: " + mode;
+                throw new IllegalStateException("Invalid column write node: " + mode);
         }
-        
+
         return val;
     }
-    
+
     /**
      * Writes reference value to row.
      *
@@ -284,99 +292,99 @@ abstract class FieldAccessor {
      */
     private static void writeRefObject(Object val, RowAssembler writer, BinaryMode mode) {
         assert writer != null;
-        
+
         if (val == null) {
             writer.appendNull();
-            
+
             return;
         }
-        
+
         switch (mode) {
             case BYTE:
                 writer.appendByte((Byte) val);
-                
+
                 break;
-            
+
             case SHORT:
                 writer.appendShort((Short) val);
-                
+
                 break;
-            
+
             case INT:
                 writer.appendInt((Integer) val);
-                
+
                 break;
-            
+
             case LONG:
                 writer.appendLong((Long) val);
-                
+
                 break;
-            
+
             case FLOAT:
                 writer.appendFloat((Float) val);
-                
+
                 break;
-            
+
             case DOUBLE:
                 writer.appendDouble((Double) val);
-                
+
                 break;
-            
+
             case STRING:
                 writer.appendString((String) val);
-                
+
                 break;
-            
+
             case UUID:
                 writer.appendUuid((UUID) val);
-                
+
                 break;
-            
+
             case BYTE_ARR:
                 writer.appendBytes((byte[]) val);
-                
+
                 break;
-            
+
             case BITSET:
                 writer.appendBitmask((BitSet) val);
-                
+
                 break;
-            
+
             case NUMBER:
                 writer.appendNumber((BigInteger) val);
-                
+
                 break;
-            
+
             case DECIMAL:
                 writer.appendDecimal((BigDecimal) val);
-                
+
                 break;
-            
+
             case DATE:
                 writer.appendDate((LocalDate) val);
-                
+
                 break;
-            
+
             case TIME:
                 writer.appendTime((LocalTime) val);
-                
+
                 break;
-            
+
             case TIMESTAMP:
                 writer.appendTimestamp((Instant) val);
-                
+
                 break;
-            
+
             case DATETIME:
                 writer.appendDateTime((LocalDateTime) val);
-                
+
                 break;
-            
+
             default:
-                assert false : "Invalid mode: " + mode;
+                throw new IllegalStateException("Invalid column write node: " + mode);
         }
     }
-    
+
     /**
      * Constructor.
      *
@@ -386,12 +394,12 @@ abstract class FieldAccessor {
      */
     protected FieldAccessor(VarHandle varHandle, int colIdx, BinaryMode mode) {
         assert colIdx >= 0;
-        
+
         this.colIdx = colIdx;
         this.mode = Objects.requireNonNull(mode);
         this.varHandle = Objects.requireNonNull(varHandle);
     }
-    
+
     /**
      * Constructor.
      *
@@ -400,12 +408,12 @@ abstract class FieldAccessor {
      */
     private FieldAccessor(int colIdx, BinaryMode mode) {
         assert colIdx >= 0;
-        
+
         this.colIdx = colIdx;
         this.mode = mode;
         varHandle = null;
     }
-    
+
     /**
      * Write object field value to row.
      *
@@ -420,7 +428,7 @@ abstract class FieldAccessor {
             throw new MarshallerException("Failed to write field [id=" + colIdx + ']', ex);
         }
     }
-    
+
     /**
      * Write object field value to row.
      *
@@ -429,7 +437,7 @@ abstract class FieldAccessor {
      * @throws Exception If write failed.
      */
     protected abstract void write0(RowAssembler writer, Object obj) throws Exception;
-    
+
     /**
      * Reads value fom row to object field.
      *
@@ -444,17 +452,17 @@ abstract class FieldAccessor {
             throw new MarshallerException("Failed to read field [id=" + colIdx + ']', ex);
         }
     }
-    
+
     /**
      * Read an object from a row.
      *
      * @param reader Row reader.
      * @return Object.
      */
-    public Object read(Row reader) {
+    public Object read(Row reader) throws MarshallerException {
         throw new UnsupportedOperationException();
     }
-    
+
     /**
      * Reads value fom row to object field.
      *
@@ -463,7 +471,7 @@ abstract class FieldAccessor {
      * @throws Exception If failed.
      */
     protected abstract void read0(Row reader, Object obj) throws Exception;
-    
+
     /**
      * Reads object field value.
      *
@@ -473,14 +481,14 @@ abstract class FieldAccessor {
     Object value(Object obj) {
         return varHandle.get(Objects.requireNonNull(obj));
     }
-    
+
     /**
      * Stubbed accessor for unused columns writes default column value, and ignore value on read access.
      */
     private static class UnmappedFieldAccessor extends FieldAccessor {
         /** Column. */
         private final Column col;
-        
+
         /**
          * Constructor.
          *
@@ -490,26 +498,26 @@ abstract class FieldAccessor {
             super(0, null);
             this.col = col;
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void read0(Row reader, Object obj) {
             // No-op.
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void write0(RowAssembler writer, Object obj) {
             RowAssembler.writeValue(writer, col, col.defaultValue());
         }
-        
+
         /** {@inheritDoc} */
         @Override
         Object value(Object obj) {
             return col.defaultValue();
         }
     }
-    
+
     /**
      * Accessor for a field of primitive {@code byte} type.
      */
@@ -523,32 +531,90 @@ abstract class FieldAccessor {
         IdentityAccessor(int colIdx, BinaryMode mode) {
             super(colIdx, mode);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void write0(RowAssembler writer, Object obj) {
             writeRefObject(obj, writer, mode);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void read0(Row reader, Object obj) {
             throw new UnsupportedOperationException("Called identity accessor for object field.");
         }
-        
+
         /** {@inheritDoc} */
         @Override
         public Object read(Row reader) {
             return readRefValue(reader, colIdx, mode);
         }
-        
+
+        /** {@inheritDoc} */
+        @Override
+        Object value(Object obj) {
+            return obj;
+        }
+    }
+
+    /**
+     * Accessor for a field of primitive {@code byte} type.
+     */
+    private static class PojoIdentityAccessor extends FieldAccessor {
+        /**
+         * Constructor.
+         *
+         * @param colIdx Column index.
+         */
+        PojoIdentityAccessor(int colIdx) {
+            super(colIdx, BinaryMode.BYTE_ARR);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void write0(RowAssembler writer, Object obj) throws Exception {
+            assert obj != null;
+
+            ByteArrayOutputStream out = new ByteArrayOutputStream(512);
+
+            try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
+                oos.writeObject(obj);
+            }
+
+            obj = out.toByteArray();
+
+            writeRefObject(obj, writer, BinaryMode.BYTE_ARR);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void read0(Row reader, Object obj) {
+            throw new UnsupportedOperationException("Called identity accessor for object field.");
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public Object read(Row reader) throws MarshallerException {
+            Object val = readRefValue(reader, colIdx, BinaryMode.BYTE_ARR);
+
+            if (val != null) {
+                try (ObjectInputStream is = new ObjectInputStream(new ByteArrayInputStream((byte[]) val))) {
+                    val = is.readObject();
+                } catch (IOException | ClassNotFoundException e) {
+                    throw new MarshallerException(e);
+                }
+            }
+
+            return val;
+        }
+
         /** {@inheritDoc} */
         @Override
         Object value(Object obj) {
             return obj;
         }
     }
-    
+
     /**
      * Accessor for a field of primitive {@code byte} type.
      */
@@ -562,24 +628,24 @@ abstract class FieldAccessor {
         BytePrimitiveAccessor(VarHandle varHandle, int colIdx) {
             super(Objects.requireNonNull(varHandle), colIdx, BinaryMode.P_BYTE);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void write0(RowAssembler writer, Object obj) {
             final byte val = (byte) varHandle.get(obj);
-            
+
             writer.appendByte(val);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void read0(Row reader, Object obj) {
             final byte val = reader.byteValue(colIdx);
-            
+
             varHandle.set(obj, val);
         }
     }
-    
+
     /**
      * Accessor for a field of primitive {@code short} type.
      */
@@ -593,24 +659,24 @@ abstract class FieldAccessor {
         ShortPrimitiveAccessor(VarHandle varHandle, int colIdx) {
             super(Objects.requireNonNull(varHandle), colIdx, BinaryMode.P_SHORT);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void write0(RowAssembler writer, Object obj) {
             final short val = (short) varHandle.get(obj);
-            
+
             writer.appendShort(val);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void read0(Row reader, Object obj) {
             final short val = reader.shortValue(colIdx);
-            
+
             varHandle.set(obj, val);
         }
     }
-    
+
     /**
      * Accessor for a field of primitive {@code int} type.
      */
@@ -624,24 +690,24 @@ abstract class FieldAccessor {
         IntPrimitiveAccessor(VarHandle varHandle, int colIdx) {
             super(Objects.requireNonNull(varHandle), colIdx, BinaryMode.P_INT);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void write0(RowAssembler writer, Object obj) {
             final int val = (int) varHandle.get(obj);
-            
+
             writer.appendInt(val);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void read0(Row reader, Object obj) {
             final int val = reader.intValue(colIdx);
-            
+
             varHandle.set(obj, val);
         }
     }
-    
+
     /**
      * Accessor for a field of primitive {@code long} type.
      */
@@ -655,24 +721,24 @@ abstract class FieldAccessor {
         LongPrimitiveAccessor(VarHandle varHandle, int colIdx) {
             super(Objects.requireNonNull(varHandle), colIdx, BinaryMode.P_LONG);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void write0(RowAssembler writer, Object obj) {
             final long val = (long) varHandle.get(obj);
-            
+
             writer.appendLong(val);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void read0(Row reader, Object obj) {
             final long val = reader.longValue(colIdx);
-            
+
             varHandle.set(obj, val);
         }
     }
-    
+
     /**
      * Accessor for a field of primitive {@code float} type.
      */
@@ -686,24 +752,24 @@ abstract class FieldAccessor {
         FloatPrimitiveAccessor(VarHandle varHandle, int colIdx) {
             super(Objects.requireNonNull(varHandle), colIdx, BinaryMode.P_FLOAT);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void write0(RowAssembler writer, Object obj) {
             final float val = (float) varHandle.get(obj);
-            
+
             writer.appendFloat(val);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void read0(Row reader, Object obj) {
             final float val = reader.floatValue(colIdx);
-            
+
             varHandle.set(obj, val);
         }
     }
-    
+
     /**
      * Accessor for a field of primitive {@code double} type.
      */
@@ -717,24 +783,24 @@ abstract class FieldAccessor {
         DoublePrimitiveAccessor(VarHandle varHandle, int colIdx) {
             super(Objects.requireNonNull(varHandle), colIdx, BinaryMode.P_DOUBLE);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void write0(RowAssembler writer, Object obj) {
             final double val = (double) varHandle.get(obj);
-            
+
             writer.appendDouble(val);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void read0(Row reader, Object obj) {
             final double val = reader.doubleValue(colIdx);
-            
+
             varHandle.set(obj, val);
         }
     }
-    
+
     /**
      * Accessor for a field of reference type.
      */
@@ -749,29 +815,76 @@ abstract class FieldAccessor {
         ReferenceFieldAccessor(VarHandle varHandle, int colIdx, BinaryMode mode) {
             super(Objects.requireNonNull(varHandle), colIdx, mode);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         protected void write0(RowAssembler writer, Object obj) {
             assert obj != null;
             assert writer != null;
-            
+
             Object val = varHandle.get(obj);
-            
-            if (val == null) {
-                writer.appendNull();
-                
-                return;
-            }
-            
+
             writeRefObject(val, writer, mode);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         public void read0(Row reader, Object obj) {
             Object val = readRefValue(reader, colIdx, mode);
-            
+
+            varHandle.set(obj, val);
+        }
+    }
+
+    /**
+     * Accessor for a field of reference type.
+     */
+    private static class PojoFieldAccessor extends FieldAccessor {
+        /**
+         * Constructor.
+         *
+         * @param varHandle VarHandle.
+         * @param colIdx    Column index.
+         */
+        PojoFieldAccessor(VarHandle varHandle, int colIdx) {
+            super(Objects.requireNonNull(varHandle), colIdx, BinaryMode.BYTE_ARR);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        protected void write0(RowAssembler writer, Object obj) throws MarshallerException {
+            assert obj != null;
+
+            Object val = varHandle.get(obj);
+
+            if (val != null) {
+                ByteArrayOutputStream out = new ByteArrayOutputStream(512);
+
+                try (ObjectOutputStream oos = new ObjectOutputStream(out)) {
+                    oos.writeObject(val);
+                } catch (IOException e) {
+                    throw new MarshallerException(e);
+                }
+
+                val = out.toByteArray();
+            }
+
+            writeRefObject(val, writer, BinaryMode.BYTE_ARR);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void read0(Row reader, Object obj) throws MarshallerException {
+            Object val = readRefValue(reader, colIdx, BinaryMode.BYTE_ARR);
+
+            if (val != null) {
+                try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream((byte[]) val))) {
+                    val = ois.readObject();
+                } catch (IOException | ClassNotFoundException e) {
+                    throw new MarshallerException(e);
+                }
+            }
+
             varHandle.set(obj, val);
         }
     }
diff --git a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/Marshaller.java b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/Marshaller.java
index cbe2137..e469f7f 100644
--- a/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/Marshaller.java
+++ b/modules/schema/src/main/java/org/apache/ignite/internal/schema/marshaller/reflection/Marshaller.java
@@ -17,9 +17,11 @@
 
 package org.apache.ignite.internal.schema.marshaller.reflection;
 
+import java.util.Arrays;
 import java.util.Objects;
 import org.apache.ignite.internal.schema.Column;
 import org.apache.ignite.internal.schema.Columns;
+import org.apache.ignite.internal.schema.SchemaMismatchException;
 import org.apache.ignite.internal.schema.marshaller.BinaryMode;
 import org.apache.ignite.internal.schema.marshaller.MarshallerException;
 import org.apache.ignite.internal.schema.marshaller.MarshallerUtil;
@@ -43,33 +45,39 @@ public abstract class Marshaller {
      */
     public static <T> Marshaller createMarshaller(Column[] cols, Mapper<T> mapper) {
         final BinaryMode mode = MarshallerUtil.mode(mapper.targetType());
-        
-        if (mode != null) {
-            final Column col = cols[0];
-            
-            assert cols.length == 1;
-            assert mode.typeSpec() == col.type().spec() : "Target type is not compatible.";
+
+        if (mode != BinaryMode.POJO || mapper.mappedColumn() != null) {
+            Column col = (mapper.mappedColumn() == null && cols.length == 1) ? cols[0] :
+                    Arrays.stream(cols).filter(c -> c.name().equals(mapper.mappedColumn())).findFirst().orElseThrow(() ->
+                    new SchemaMismatchException("Failed to map object to a single column: mappedColumn=" + mapper.mappedColumn()));
+
+            if (mode.typeSpec() != col.type().spec()) {
+                throw new SchemaMismatchException(
+                        String.format("Object can't be mapped to a column of incompatible type: columnType=%s, mappedType=%s",
+                                col.type().spec(), mapper.targetType().getName()));
+            }
+
             assert !mapper.targetType().isPrimitive() : "Non-nullable types are not allowed.";
-            
+
             return new SimpleMarshaller(FieldAccessor.createIdentityAccessor(col, col.schemaIndex(), mode));
         }
-        
+
         FieldAccessor[] fieldAccessors = new FieldAccessor[cols.length];
-        
+
         // Build handlers.
         for (int i = 0; i < cols.length; i++) {
             final Column col = cols[i];
-            
-            String fieldName = mapper.columnToField(col.name());
-            
+
+            String fieldName = mapper.mappedField(col.name());
+
             // TODO: IGNITE-15785 validate key marshaller has no NoopAccessors.
             fieldAccessors[i] = (fieldName == null) ? FieldAccessor.noopAccessor(col) :
                     FieldAccessor.create(mapper.targetType(), fieldName, col, col.schemaIndex());
         }
-        
+
         return new PojoMarshaller(new ObjectFactory<>(mapper.targetType()), fieldAccessors);
     }
-    
+
     /**
      * Creates a marshaller for class.
      *
@@ -81,29 +89,29 @@ public abstract class Marshaller {
     @Deprecated
     public static Marshaller createMarshaller(Columns cols, Class<? extends Object> cls) {
         final BinaryMode mode = MarshallerUtil.mode(cls);
-        
-        if (mode != null) {
+
+        if (mode != BinaryMode.POJO) {
             final Column col = cols.column(0);
-            
+
             assert cols.length() == 1;
             assert mode.typeSpec() == col.type().spec() : "Target type is not compatible.";
             assert !cls.isPrimitive() : "Non-nullable types are not allowed.";
-            
+
             return new SimpleMarshaller(FieldAccessor.createIdentityAccessor(col, col.schemaIndex(), mode));
         }
-        
+
         FieldAccessor[] fieldAccessors = new FieldAccessor[cols.length()];
-        
+
         // Build accessors
         for (int i = 0; i < cols.length(); i++) {
             final Column col = cols.column(i);
-            
+
             fieldAccessors[i] = FieldAccessor.create(cls, col.name(), col, col.schemaIndex());
         }
-        
+
         return new PojoMarshaller(new ObjectFactory<>(cls), fieldAccessors);
     }
-    
+
     /**
      * Reads object field value.
      *
@@ -112,7 +120,7 @@ public abstract class Marshaller {
      * @return Field value.
      */
     public abstract @Nullable Object value(Object obj, int fldIdx);
-    
+
     /**
      * Reads object from a row.
      *
@@ -121,7 +129,7 @@ public abstract class Marshaller {
      * @throws MarshallerException If failed.
      */
     public abstract Object readObject(Row reader) throws MarshallerException;
-    
+
     /**
      * Write an object to a row.
      *
@@ -130,14 +138,14 @@ public abstract class Marshaller {
      * @throws MarshallerException If failed.
      */
     public abstract void writeObject(Object obj, RowAssembler writer) throws MarshallerException;
-    
+
     /**
      * Marshaller for objects of natively supported types.
      */
     static class SimpleMarshaller extends Marshaller {
         /** Identity accessor. */
         private final FieldAccessor fieldAccessor;
-        
+
         /**
          * Creates a marshaller for objects of natively supported type.
          *
@@ -146,40 +154,40 @@ public abstract class Marshaller {
         SimpleMarshaller(FieldAccessor fieldAccessor) {
             this.fieldAccessor = fieldAccessor;
         }
-        
+
         /** {@inheritDoc} */
         @Override
         public @Nullable
         Object value(Object obj, int fldIdx) {
             assert fldIdx == 0;
-            
+
             return fieldAccessor.value(obj);
         }
-        
+
         /** {@inheritDoc} */
         @Override
-        public Object readObject(Row reader) {
+        public Object readObject(Row reader) throws MarshallerException {
             return fieldAccessor.read(reader);
         }
-        
-        
+
+
         /** {@inheritDoc} */
         @Override
         public void writeObject(Object obj, RowAssembler writer) throws MarshallerException {
             fieldAccessor.write(writer, obj);
         }
     }
-    
+
     /**
      * Marshaller for POJOs.
      */
     static class PojoMarshaller extends Marshaller {
         /** Field accessors for mapped columns. Array has same size and order as columns. */
         private final FieldAccessor[] fieldAccessors;
-        
+
         /** Object factory. */
         private final Factory<?> factory;
-        
+
         /**
          * Creates a marshaller for POJOs.
          *
@@ -191,26 +199,26 @@ public abstract class Marshaller {
             this.fieldAccessors = fieldAccessors;
             this.factory = Objects.requireNonNull(factory);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         public @Nullable
         Object value(Object obj, int fldIdx) {
             return fieldAccessors[fldIdx].value(obj);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         public Object readObject(Row reader) throws MarshallerException {
             final Object obj = factory.create();
-            
+
             for (int fldIdx = 0; fldIdx < fieldAccessors.length; fldIdx++) {
                 fieldAccessors[fldIdx].read(reader, obj);
             }
-            
+
             return obj;
         }
-        
+
         /** {@inheritDoc} */
         @Override
         public void writeObject(Object obj, RowAssembler writer)
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
index 0a024ad..0a18aff 100644
--- a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/KvMarshallerTest.java
@@ -47,6 +47,10 @@ import com.facebook.presto.bytecode.MethodDefinition;
 import com.facebook.presto.bytecode.ParameterizedType;
 import com.facebook.presto.bytecode.Variable;
 import com.facebook.presto.bytecode.expression.BytecodeExpressions;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectOutputStream;
+import java.io.Serializable;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.EnumSet;
@@ -89,27 +93,27 @@ public class KvMarshallerTest {
     private static List<MarshallerFactory> marshallerFactoryProvider() {
         return List.of(new ReflectionMarshallerFactory());
     }
-    
+
     /** Random. */
     private Random rnd;
-    
+
     /**
      * Init test.
      */
     @BeforeEach
     public void initRandom() {
         long seed = System.currentTimeMillis();
-        
+
         System.out.println("Using seed: " + seed + "L;");
-        
+
         rnd = new Random(seed);
     }
-    
+
     @TestFactory
     public Stream<DynamicNode> basicTypes() {
         NativeType[] types = new NativeType[]{INT8, INT16, INT32, INT64, FLOAT, DOUBLE, UUID, STRING, BYTES,
                 NativeTypes.bitmaskOf(5), NativeTypes.numberOf(42), NativeTypes.decimalOf(12, 3)};
-        
+
         return marshallerFactoryProvider().stream().map(factory ->
                 dynamicContainer(
                         factory.getClass().getSimpleName(),
@@ -118,7 +122,7 @@ public class KvMarshallerTest {
                                 Stream.of(types).map(type ->
                                         dynamicTest("testBasicTypes(" + type.spec().name() + ')', () -> checkBasicType(factory, type, type))
                                 ),
-                                
+
                                 // Test pairs of mixed types.
                                 Stream.of(
                                         dynamicTest("testMixTypes 1", () -> checkBasicType(factory, INT64, INT32)),
@@ -132,57 +136,57 @@ public class KvMarshallerTest {
                         )
                 ));
     }
-    
+
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void pojoWithFieldsOfAllTypes(MarshallerFactory factory) throws MarshallerException {
         Column[] cols = columnsAllTypes();
-        
+
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
-        
+
         final TestObjectWithAllTypes key = TestObjectWithAllTypes.randomObject(rnd);
         final TestObjectWithAllTypes val = TestObjectWithAllTypes.randomObject(rnd);
-        
+
         KvMarshaller<TestObjectWithAllTypes, TestObjectWithAllTypes> marshaller =
                 factory.create(schema, TestObjectWithAllTypes.class, TestObjectWithAllTypes.class);
-        
+
         BinaryRow row = marshaller.marshal(key, val);
-        
+
         TestObjectWithAllTypes restoredVal = marshaller.unmarshalValue(new Row(schema, row));
         TestObjectWithAllTypes restoredKey = marshaller.unmarshalKey(new Row(schema, row));
-        
+
         assertTrue(key.getClass().isInstance(restoredKey));
         assertTrue(val.getClass().isInstance(restoredVal));
-        
+
         assertEquals(key, restoredKey);
         assertEquals(val, restoredVal);
     }
-    
+
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void narrowType(MarshallerFactory factory) throws MarshallerException {
         Column[] cols = columnsAllTypes();
-        
+
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
-        
+
         KvMarshaller<TestTruncatedObject, TestTruncatedObject> marshaller =
                 factory.create(schema, TestTruncatedObject.class, TestTruncatedObject.class);
-        
+
         final TestTruncatedObject key = TestTruncatedObject.randomObject(rnd);
         final TestTruncatedObject val = TestTruncatedObject.randomObject(rnd);
-        
+
         BinaryRow row = marshaller.marshal(key, val);
-        
+
         Object restoredVal = marshaller.unmarshalValue(new Row(schema, row));
         Object restoredKey = marshaller.unmarshalKey(new Row(schema, row));
-        
+
         assertTrue(key.getClass().isInstance(restoredKey));
         assertTrue(val.getClass().isInstance(restoredVal));
-        
+
         assertEquals(key, restoredKey);
         assertEquals(val, restoredVal);
     }
-    
+
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void wideType(MarshallerFactory factory) throws MarshallerException {
@@ -191,43 +195,43 @@ public class KvMarshallerTest {
                 new Column("primitiveDoubleCol", DOUBLE, false),
                 new Column("stringCol", STRING, true),
         };
-        
+
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
-        
+
         KvMarshaller<TestObjectWithAllTypes, TestObjectWithAllTypes> marshaller =
                 factory.create(schema, TestObjectWithAllTypes.class, TestObjectWithAllTypes.class);
-        
+
         final TestObjectWithAllTypes key = TestObjectWithAllTypes.randomObject(rnd);
         final TestObjectWithAllTypes val = TestObjectWithAllTypes.randomObject(rnd);
-        
+
         BinaryRow row = marshaller.marshal(key, val);
-        
+
         TestObjectWithAllTypes restoredVal = marshaller.unmarshalValue(new Row(schema, row));
         TestObjectWithAllTypes restoredKey = marshaller.unmarshalKey(new Row(schema, row));
-        
+
         assertTrue(key.getClass().isInstance(restoredKey));
         assertTrue(val.getClass().isInstance(restoredVal));
-        
+
         TestObjectWithAllTypes expectedKey = new TestObjectWithAllTypes();
         expectedKey.setPrimitiveLongCol(key.getPrimitiveLongCol());
         expectedKey.setPrimitiveDoubleCol(key.getPrimitiveDoubleCol());
         expectedKey.setStringCol(key.getStringCol());
-        
+
         TestObjectWithAllTypes expectedVal = new TestObjectWithAllTypes();
         expectedVal.setPrimitiveLongCol(val.getPrimitiveLongCol());
         expectedVal.setPrimitiveDoubleCol(val.getPrimitiveDoubleCol());
         expectedVal.setStringCol(val.getStringCol());
-        
+
         assertEquals(expectedKey, restoredKey);
         assertEquals(expectedVal, restoredVal);
-        
+
         // Check non-mapped fields has default values.
         assertNull(restoredKey.getUuidCol());
         assertNull(restoredVal.getUuidCol());
         assertEquals(0, restoredKey.getPrimitiveIntCol());
         assertEquals(0, restoredVal.getPrimitiveIntCol());
     }
-    
+
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void columnNameMapping(MarshallerFactory factory) throws MarshallerException {
@@ -238,35 +242,35 @@ public class KvMarshallerTest {
                         new Column("col2", INT64, true),
                         new Column("col3", STRING, false)
                 });
-        
+
         Mapper<TestKeyObject> keyMapper = Mapper.builderFor(TestKeyObject.class)
                 .map("id", "key")
                 .build();
-        
+
         Mapper<TestObject> valMapper = Mapper.builderFor(TestObject.class)
                 .map("longCol", "col1")
                 .map("stringCol", "col3")
                 .build();
-        
+
         KvMarshaller<TestKeyObject, TestObject> marshaller = factory.create(schema, keyMapper, valMapper);
-        
+
         final TestKeyObject key = TestKeyObject.randomObject(rnd);
         final TestObject val = TestObject.randomObject(rnd);
-        
+
         BinaryRow row = marshaller.marshal(key, val);
-        
+
         Object restoredVal = marshaller.unmarshalValue(new Row(schema, row));
         Object restoredKey = marshaller.unmarshalKey(new Row(schema, row));
-        
+
         assertTrue(key.getClass().isInstance(restoredKey));
         assertTrue(val.getClass().isInstance(restoredVal));
-        
+
         val.longCol2 = null;
-        
+
         assertEquals(key, restoredKey);
         assertEquals(val, restoredVal);
     }
-    
+
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void classWithWrongFieldType(MarshallerFactory factory) {
@@ -274,22 +278,22 @@ public class KvMarshallerTest {
                 new Column("bitmaskCol", NativeTypes.bitmaskOf(42), true),
                 new Column("shortCol", UUID, true)
         };
-        
+
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
-        
+
         KvMarshaller<TestObjectWithAllTypes, TestObjectWithAllTypes> marshaller =
                 factory.create(schema, TestObjectWithAllTypes.class, TestObjectWithAllTypes.class);
-        
+
         final TestObjectWithAllTypes key = TestObjectWithAllTypes.randomObject(rnd);
         final TestObjectWithAllTypes val = TestObjectWithAllTypes.randomObject(rnd);
-        
+
         assertThrows(
                 MarshallerException.class,
                 () -> marshaller.marshal(key, val),
                 "Failed to write field [name=shortCol]"
         );
     }
-    
+
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void classWithIncorrectBitmaskSize(MarshallerFactory factory) {
@@ -297,22 +301,22 @@ public class KvMarshallerTest {
                 new Column("primitiveLongCol", INT64, false),
                 new Column("bitmaskCol", NativeTypes.bitmaskOf(9), true),
         };
-        
+
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
-        
+
         KvMarshaller<TestObjectWithAllTypes, TestObjectWithAllTypes> marshaller =
                 factory.create(schema, TestObjectWithAllTypes.class, TestObjectWithAllTypes.class);
-        
+
         final TestObjectWithAllTypes key = TestObjectWithAllTypes.randomObject(rnd);
         final TestObjectWithAllTypes val = TestObjectWithAllTypes.randomObject(rnd);
-        
+
         assertThrows(
                 MarshallerException.class,
                 () -> marshaller.marshal(key, val),
                 "Failed to write field [name=bitmaskCol]"
         );
     }
-    
+
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void classWithPrivateConstructor(MarshallerFactory factory) throws MarshallerException {
@@ -320,39 +324,39 @@ public class KvMarshallerTest {
                 new Column("primLongCol", INT64, false),
                 new Column("primIntCol", INT32, false),
         };
-        
+
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
-        
+
         KvMarshaller<TestObjectWithPrivateConstructor, TestObjectWithPrivateConstructor> marshaller =
                 factory.create(schema, TestObjectWithPrivateConstructor.class, TestObjectWithPrivateConstructor.class);
-        
+
         final TestObjectWithPrivateConstructor key = TestObjectWithPrivateConstructor.randomObject(rnd);
         final TestObjectWithPrivateConstructor val = TestObjectWithPrivateConstructor.randomObject(rnd);
-        
+
         BinaryRow row = marshaller.marshal(key, val);
-        
+
         Object key1 = marshaller.unmarshalKey(new Row(schema, row));
         Object val1 = marshaller.unmarshalValue(new Row(schema, row));
-        
+
         assertTrue(key.getClass().isInstance(key1));
         assertTrue(val.getClass().isInstance(val1));
-        
+
         assertEquals(key, key);
         assertEquals(val, val1);
     }
-    
+
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void classWithNoDefaultConstructor(MarshallerFactory factory) {
         Column[] cols = new Column[]{
                 new Column("primLongCol", INT64, false),
         };
-        
+
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
-        
+
         final Object key = TestObjectWithNoDefaultConstructor.randomObject(rnd);
         final Object val = TestObjectWithNoDefaultConstructor.randomObject(rnd);
-        
+
         assertThrows(IgniteInternalException.class, () -> factory.create(schema, key.getClass(), val.getClass()));
     }
 
@@ -362,64 +366,116 @@ public class KvMarshallerTest {
         Column[] cols = new Column[]{
                 new Column("primLongCol", INT64, false),
         };
-        
+
         SchemaDescriptor schema = new SchemaDescriptor(1, cols, cols);
-        
+
         final ObjectFactory<PrivateTestObject> objFactory = new ObjectFactory<>(PrivateTestObject.class);
         final KvMarshaller<PrivateTestObject, PrivateTestObject> marshaller =
                 factory.create(schema, PrivateTestObject.class, PrivateTestObject.class);
-        
+
         final PrivateTestObject key = PrivateTestObject.randomObject(rnd);
         final PrivateTestObject val = PrivateTestObject.randomObject(rnd);
-        
+
         BinaryRow row = marshaller.marshal(key, objFactory.create());
-        
+
         Object key1 = marshaller.unmarshalKey(new Row(schema, row));
         Object val1 = marshaller.unmarshalValue(new Row(schema, row));
-        
+
         assertTrue(key.getClass().isInstance(key1));
         assertTrue(val.getClass().isInstance(val1));
     }
-    
+
     @ParameterizedTest
     @MethodSource("marshallerFactoryProvider")
     public void classLoader(MarshallerFactory factory) throws MarshallerException {
         final ClassLoader loader = Thread.currentThread().getContextClassLoader();
         try {
             Thread.currentThread().setContextClassLoader(new DynamicClassLoader(getClass().getClassLoader()));
-            
+
             Column[] keyCols = new Column[]{
                     new Column("key", INT64, false)
             };
-            
+
             Column[] valCols = new Column[]{
                     new Column("col0", INT64, false),
                     new Column("col1", INT64, false),
                     new Column("col2", INT64, false),
             };
-            
+
             SchemaDescriptor schema = new SchemaDescriptor(1, keyCols, valCols);
-            
+
             final Class<?> valClass = createGeneratedObjectClass();
             final ObjectFactory<?> objFactory = new ObjectFactory<>(valClass);
-            
+
             KvMarshaller<Long, Object> marshaller = factory.create(schema, Long.class, (Class<Object>) valClass);
-            
+
             final Long key = rnd.nextLong();
-            
+
             BinaryRow row = marshaller.marshal(key, objFactory.create());
-            
+
             Long key1 = marshaller.unmarshalKey(new Row(schema, row));
             Object val1 = marshaller.unmarshalValue(new Row(schema, row));
-            
+
             assertTrue(valClass.isInstance(val1));
-            
+
             assertEquals(key, key1);
         } finally {
             Thread.currentThread().setContextClassLoader(loader);
         }
     }
-    
+
+    @ParameterizedTest
+    @MethodSource("marshallerFactoryProvider")
+    public void pojoMapping(MarshallerFactory factory) throws MarshallerException, IOException {
+        final SchemaDescriptor schema = new SchemaDescriptor(
+                1,
+                new Column[]{new Column("key", INT64, false)},
+                new Column[]{new Column("val", BYTES, true),
+                });
+
+        final TestPojo pojo = new TestPojo(42);
+        final byte[] serializedPojo = serializeObject(pojo);
+
+        final KvMarshaller<Long, TestPojo> marshaller1 = factory.create(schema,
+                Mapper.of("key", Long.class), Mapper.of("val", TestPojo.class));
+        final KvMarshaller<Long, byte[]> marshaller2 = factory.create(schema,
+                Mapper.of("key", Long.class), Mapper.of("val", byte[].class));
+        final KvMarshaller<Long, TestPojoWrapper> marshaller3 = factory.create(schema,
+                Mapper.of("key", Long.class), Mapper.builderFor(TestPojoWrapper.class).map("pojoField", "val").build());
+        final KvMarshaller<Long, TestPojoWrapper> marshaller4 = factory.create(schema,
+                Mapper.of("key", Long.class), Mapper.builderFor(TestPojoWrapper.class).map("rawField", "val").build());
+
+        BinaryRow row = marshaller1.marshal(1L, pojo);
+        BinaryRow row2 = marshaller2.marshal(1L, serializedPojo);
+        BinaryRow row3= marshaller3.marshal(1L, new TestPojoWrapper(pojo));
+        BinaryRow row4 = marshaller4.marshal(1L, new TestPojoWrapper(serializedPojo));
+
+        // Verify all rows are equivalent.
+        assertArrayEquals(row.bytes(), row2.bytes());
+        assertArrayEquals(row.bytes(), row3.bytes());
+        assertArrayEquals(row.bytes(), row4.bytes());
+
+        // Check key.
+        assertEquals(1L, marshaller1.unmarshalKey(new Row(schema, row)));
+        assertEquals(1L, marshaller2.unmarshalKey(new Row(schema, row)));
+        assertEquals(1L, marshaller3.unmarshalKey(new Row(schema, row)));
+        assertEquals(1L, marshaller4.unmarshalKey(new Row(schema, row)));
+
+        // Check values.
+        assertEquals(pojo,  marshaller1.unmarshalValue(new Row(schema, row)));
+        assertArrayEquals(serializedPojo,  marshaller2.unmarshalValue(new Row(schema, row)));
+        assertEquals(new TestPojoWrapper(pojo),  marshaller3.unmarshalValue(new Row(schema, row)));
+        assertEquals(new TestPojoWrapper(serializedPojo),  marshaller4.unmarshalValue(new Row(schema, row)));
+    }
+
+    public byte[] serializeObject(TestPojo obj) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
+        try (ObjectOutputStream dos = new ObjectOutputStream(baos)) {
+            dos.writeObject(obj);
+        }
+        return baos.toByteArray();
+    }
+
     /**
      * Generate random key-value pair of given types and check serialization and deserialization works fine.
      *
@@ -432,26 +488,26 @@ public class KvMarshallerTest {
             NativeType valType) throws MarshallerException {
         final Object key = generateRandomValue(keyType);
         final Object val = generateRandomValue(valType);
-        
+
         Column[] keyCols = new Column[]{new Column("key", keyType, false)};
         Column[] valCols = new Column[]{new Column("val", valType, false)};
-        
+
         SchemaDescriptor schema = new SchemaDescriptor(1, keyCols, valCols);
-        
+
         KvMarshaller<Object, Object> marshaller = factory.create(schema, (Class<Object>) key.getClass(), (Class<Object>) val.getClass());
-        
+
         BinaryRow row = marshaller.marshal(key, val);
-        
+
         Object key1 = marshaller.unmarshalKey(new Row(schema, row));
         Object val1 = marshaller.unmarshalValue(new Row(schema, row));
-        
+
         assertTrue(key.getClass().isInstance(key1));
         assertTrue(val.getClass().isInstance(val1));
-        
+
         compareObjects(keyType, key, key);
         compareObjects(valType, val, val1);
     }
-    
+
     /**
      * Compare object regarding NativeType.
      *
@@ -466,7 +522,7 @@ public class KvMarshallerTest {
             assertEquals(exp, act);
         }
     }
-    
+
     /**
      * Generates random value of given type.
      *
@@ -475,7 +531,7 @@ public class KvMarshallerTest {
     private Object generateRandomValue(NativeType type) {
         return SchemaTestUtils.generateRandomValue(rnd, type);
     }
-    
+
     /**
      * Generate class for test objects.
      *
@@ -484,40 +540,40 @@ public class KvMarshallerTest {
     private Class<?> createGeneratedObjectClass() {
         final String packageName = getClass().getPackageName();
         final String className = "GeneratedTestObject";
-        
+
         final ClassDefinition classDef = new ClassDefinition(
                 EnumSet.of(Access.PUBLIC),
                 packageName.replace('.', '/') + '/' + className,
                 ParameterizedType.type(Object.class)
         );
         classDef.declareAnnotation(Generated.class).setValue("value", getClass().getCanonicalName());
-        
+
         for (int i = 0; i < 3; i++) {
             classDef.declareField(EnumSet.of(Access.PRIVATE), "col" + i, ParameterizedType.type(long.class));
         }
-        
+
         // Build constructor.
         final MethodDefinition methodDef = classDef.declareConstructor(EnumSet.of(Access.PUBLIC));
         final Variable rnd = methodDef.getScope().declareVariable(Random.class, "rnd");
-        
+
         BytecodeBlock body = methodDef.getBody()
                 .append(methodDef.getThis())
                 .invokeConstructor(classDef.getSuperClass())
                 .append(rnd.set(BytecodeExpressions.newInstance(Random.class)));
-        
+
         for (int i = 0; i < 3; i++) {
             body.append(methodDef.getThis().setField("col" + i, rnd.invoke("nextLong", long.class).cast(long.class)));
         }
-        
+
         body.ret();
-        
+
         return ClassGenerator.classGenerator(Thread.currentThread().getContextClassLoader())
                 .fakeLineNumbers(true)
                 .runAsmVerifier(true)
                 .dumpRawBytecode(true)
                 .defineClass(classDef, Object.class);
     }
-    
+
     private Column[] columnsAllTypes() {
         Column[] cols = new Column[]{
                 new Column("primitiveByteCol", INT8, false, () -> (byte) 0x42),
@@ -526,7 +582,7 @@ public class KvMarshallerTest {
                 new Column("primitiveLongCol", INT64, false),
                 new Column("primitiveFloatCol", FLOAT, false),
                 new Column("primitiveDoubleCol", DOUBLE, false),
-                
+
                 new Column("byteCol", INT8, true),
                 new Column("shortCol", INT16, true),
                 new Column("intCol", INT32, true),
@@ -534,12 +590,12 @@ public class KvMarshallerTest {
                 new Column("nullLongCol", INT64, true),
                 new Column("floatCol", FLOAT, true),
                 new Column("doubleCol", DOUBLE, true),
-                
+
                 new Column("dateCol", DATE, true),
                 new Column("timeCol", time(), true),
                 new Column("dateTimeCol", datetime(), true),
                 new Column("timestampCol", timestamp(), true),
-                
+
                 new Column("uuidCol", UUID, true),
                 new Column("bitmaskCol", NativeTypes.bitmaskOf(42), true),
                 new Column("stringCol", STRING, true),
@@ -553,11 +609,11 @@ public class KvMarshallerTest {
                 .collect(Collectors.toSet());
         Set<NativeTypeSpec> missedTypes = Arrays.stream(NativeTypeSpec.values())
                 .filter(t -> !testedTypes.contains(t)).collect(Collectors.toSet());
-        
+
         assertEquals(Collections.emptySet(), missedTypes);
         return cols;
     }
-    
+
     /**
      * Test object.
      */
@@ -565,14 +621,14 @@ public class KvMarshallerTest {
     public static class TestKeyObject {
         static TestKeyObject randomObject(Random rnd) {
             final TestKeyObject obj = new TestKeyObject();
-            
+
             obj.id = rnd.nextLong();
-            
+
             return obj;
         }
-        
+
         private long id;
-        
+
         @Override
         public boolean equals(Object o) {
             if (this == o) {
@@ -584,14 +640,14 @@ public class KvMarshallerTest {
             TestKeyObject that = (TestKeyObject) o;
             return id == that.id;
         }
-        
+
         @Override
         public int hashCode() {
             return Objects.hash(id);
         }
     }
-    
-    
+
+
     /**
      * Test object.
      */
@@ -599,43 +655,43 @@ public class KvMarshallerTest {
     public static class TestObject {
         static TestObject randomObject(Random rnd) {
             final TestObject obj = new TestObject();
-            
+
             obj.longCol = rnd.nextLong();
             obj.longCol2 = rnd.nextLong();
             obj.stringCol = IgniteTestUtils.randomString(rnd, 100);
-            
+
             return obj;
         }
-        
+
         private long longCol;
-        
+
         private Long longCol2;
-        
+
         private String stringCol;
-        
+
         @Override
         public boolean equals(Object o) {
             if (this == o) {
                 return true;
             }
-            
+
             if (o == null || getClass() != o.getClass()) {
                 return false;
             }
-            
+
             TestObject that = (TestObject) o;
-            
+
             return longCol == that.longCol
                     && Objects.equals(longCol2, that.longCol2)
                     && Objects.equals(stringCol, that.stringCol);
         }
-        
+
         @Override
         public int hashCode() {
             return Objects.hash(longCol, longCol2, stringCol);
         }
     }
-    
+
     /**
      * Test object.
      */
@@ -643,30 +699,30 @@ public class KvMarshallerTest {
     public static class TestTruncatedObject {
         static TestTruncatedObject randomObject(Random rnd) {
             final TestTruncatedObject obj = new TestTruncatedObject();
-            
+
             obj.primitiveIntCol = rnd.nextInt();
             obj.primitiveLongCol = rnd.nextLong();
             obj.primitiveDoubleCol = rnd.nextDouble();
-            
+
             obj.uuidCol = java.util.UUID.randomUUID();
             obj.stringCol = IgniteTestUtils.randomString(rnd, 100);
-            
+
             return obj;
         }
-        
+
         // Primitive typed
         private int primitiveIntCol;
-        
+
         private long primitiveLongCol;
-        
+
         private float primitiveFloatCol;
-        
+
         private double primitiveDoubleCol;
-        
+
         private String stringCol;
-        
+
         private java.util.UUID uuidCol;
-        
+
         /** {@inheritDoc} */
         @Override
         public boolean equals(Object o) {
@@ -676,9 +732,9 @@ public class KvMarshallerTest {
             if (o == null || getClass() != o.getClass()) {
                 return false;
             }
-            
+
             TestTruncatedObject object = (TestTruncatedObject) o;
-            
+
             return primitiveIntCol == object.primitiveIntCol
                     && primitiveLongCol == object.primitiveLongCol
                     && Float.compare(object.primitiveFloatCol, primitiveFloatCol) == 0
@@ -686,14 +742,14 @@ public class KvMarshallerTest {
                     && Objects.equals(stringCol, ((TestTruncatedObject) o).stringCol)
                     && Objects.equals(uuidCol, ((TestTruncatedObject) o).uuidCol);
         }
-        
+
         /** {@inheritDoc} */
         @Override
         public int hashCode() {
             return 42;
         }
     }
-    
+
     /**
      * Test object without default constructor.
      */
@@ -705,41 +761,114 @@ public class KvMarshallerTest {
         static PrivateTestObject randomObject(Random rnd) {
             return new PrivateTestObject(rnd.nextInt());
         }
-        
+
         /** Value. */
         private long primLongCol;
-        
+
         /** Constructor. */
         PrivateTestObject() {
         }
-        
+
         /**
          * Private constructor.
          */
         PrivateTestObject(long val) {
             primLongCol = val;
         }
-        
+
         /** {@inheritDoc} */
         @Override
         public boolean equals(Object o) {
             if (this == o) {
                 return true;
             }
-            
+
             if (o == null || getClass() != o.getClass()) {
                 return false;
             }
-            
+
             PrivateTestObject object = (PrivateTestObject) o;
-            
+
             return primLongCol == object.primLongCol;
         }
-        
+
         /** {@inheritDoc} */
         @Override
         public int hashCode() {
             return Objects.hash(primLongCol);
         }
     }
+
+    /**
+     * Test object represents a user object of arbitrary type.
+     */
+    static class TestPojo implements Serializable {
+        private static final long serialVersionUid = -1L;
+
+        int intField;
+
+        public TestPojo() {
+        }
+
+        public TestPojo(int intVal) {
+            this.intField = intVal;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            TestPojo testPojo = (TestPojo) o;
+            return intField == testPojo.intField;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(intField);
+        }
+    }
+
+    /**
+     * Wrapper for the {@link TestPojo}.
+     */
+    static class TestPojoWrapper {
+        TestPojo pojoField;
+
+        byte[] rawField;
+
+        public TestPojoWrapper() {
+        }
+
+        public TestPojoWrapper(TestPojo pojoField) {
+            this.pojoField = pojoField;
+        }
+
+        public TestPojoWrapper(byte[] rawField) {
+            this.rawField = rawField;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+            TestPojoWrapper that = (TestPojoWrapper) o;
+            return Objects.equals(pojoField, that.pojoField) &&
+                    Arrays.equals(rawField, that.rawField);
+        }
+
+        @Override
+        public int hashCode() {
+            int result = Objects.hash(pojoField);
+            result = 31 * result + Arrays.hashCode(rawField);
+            return result;
+        }
+    }
 }
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MapperTest.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MapperTest.java
index 1f1e90c..ddcf469 100644
--- a/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MapperTest.java
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/marshaller/MapperTest.java
@@ -17,8 +17,12 @@
 
 package org.apache.ignite.internal.schema.marshaller;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
+import java.util.function.Function;
+import org.apache.ignite.internal.schema.testobjects.TestOuterObject;
 import org.apache.ignite.table.mapper.Mapper;
 import org.apache.ignite.table.mapper.MapperBuilder;
 import org.junit.jupiter.api.Test;
@@ -27,49 +31,87 @@ import org.junit.jupiter.api.Test;
  * Columns mappers test.
  */
 public class MapperTest {
-    
+
     @Test
-    public void misleadingMapping() {
+    public void misleadingMapperUsage() {
         // Empty mapping.
         assertThrows(IllegalStateException.class, () -> Mapper.builderFor(TestObject.class).build());
-        
+
         // Many fields to one column.
         assertThrows(IllegalArgumentException.class, () -> Mapper.builderFor(TestObject.class)
                 .map("id", "key")
                 .map("longCol", "key"));
-        
+
         // One field to many columns.
         assertThrows(IllegalStateException.class, () -> Mapper.builderFor(TestObject.class)
                 .map("id", "key")
                 .map("id", "val1")
                 .map("stringCol", "val2")
                 .build());
-        
+
         // Mapper builder reuse fails.
         assertThrows(IllegalStateException.class, () -> {
             MapperBuilder<TestObject> builder = Mapper.builderFor(TestObject.class)
                     .map("id", "key");
-            
+
             builder.build();
-            
+
             builder.map("stringCol", "val2");
         });
     }
-    
+
+    @Test
+    public void supportedClassKinds() {
+        class LocalClass {
+            long id;
+        }
+
+        Function anonymous = (i) -> i;
+
+        Mapper.builderFor(TestOuterObject.class);
+        Mapper.builderFor(TestOuterObject.NestedObect.class);
+
+        assertThrows(IllegalArgumentException.class, () -> Mapper.builderFor(Long.class));
+        assertThrows(IllegalArgumentException.class, () -> Mapper.builderFor(TestOuterObject.InnerObject.class));
+        assertThrows(IllegalArgumentException.class, () -> Mapper.builderFor(LocalClass.class));
+        assertThrows(IllegalArgumentException.class, () -> Mapper.builderFor(anonymous.getClass()));
+
+        Mapper.of("key", Long.class);
+        Mapper.of("key", TestOuterObject.class);
+        Mapper.of("key", TestOuterObject.NestedObect.class);
+
+        assertThrows(IllegalArgumentException.class, () -> Mapper.of("key", TestOuterObject.InnerObject.class));
+        assertThrows(IllegalArgumentException.class, () -> Mapper.of("key", LocalClass.class));
+        assertThrows(IllegalArgumentException.class, () -> Mapper.of("key", anonymous.getClass()));
+    }
+
     @Test
     public void identityMapping() {
-        Mapper.identity(TestObject.class);
+        Mapper<TestObject> mapper = Mapper.of(TestObject.class);
+
+        assertNull(mapper.mappedColumn());
+        assertEquals("id", mapper.mappedField("id"));
+        assertNull(mapper.mappedField("val"));
     }
-    
+
+    @Test
+    public void basicMapping() {
+        Mapper<TestObject> mapper = Mapper.of(TestObject.class);
+
+        assertNull(mapper.mappedColumn());
+        assertEquals("id", mapper.mappedField("id"));
+        assertNull(mapper.mappedField("val"));
+    }
+
     /**
      * Test object.
      */
     @SuppressWarnings({"InstanceVariableMayNotBeInitialized", "unused"})
-    public static class TestObject {
+    static class TestObject {
         private long id;
-        
+
         private long longCol;
-        
+
         private String stringCol;
     }
 }
diff --git a/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestOuterObject.java b/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestOuterObject.java
new file mode 100644
index 0000000..8e1b16c
--- /dev/null
+++ b/modules/schema/src/test/java/org/apache/ignite/internal/schema/testobjects/TestOuterObject.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.schema.testobjects;
+
+/**
+ * Test object.
+ */
+public class TestOuterObject {
+    private long id;
+
+    public class InnerObject {
+        private long id1;
+    }
+
+    public static class NestedObect {
+        private long id2;
+    }
+}
diff --git a/modules/table/src/test/java/org/apache/ignite/internal/table/Example.java b/modules/table/src/test/java/org/apache/ignite/internal/table/Example.java
index b090c36..2cd31dc 100644
--- a/modules/table/src/test/java/org/apache/ignite/internal/table/Example.java
+++ b/modules/table/src/test/java/org/apache/ignite/internal/table/Example.java
@@ -22,6 +22,9 @@ import java.util.Collections;
 import java.util.List;
 import org.apache.ignite.binary.BinaryObject;
 import org.apache.ignite.binary.BinaryObjects;
+import org.apache.ignite.internal.schema.Column;
+import org.apache.ignite.internal.schema.NativeTypes;
+import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.table.impl.DummyInternalTableImpl;
 import org.apache.ignite.table.KeyValueView;
 import org.apache.ignite.table.RecordView;
@@ -35,9 +38,8 @@ import org.junit.jupiter.params.provider.MethodSource;
 /**
  * Example.
  */
-@SuppressWarnings({
-        "PMD.EmptyLineSeparatorCheck", "emptylineseparator",
-        "unused", "UnusedAssignment", "InstanceVariableMayNotBeInitialized", "JoinDeclarationAndAssignmentJava"})
+@SuppressWarnings({"PMD.EmptyLineSeparatorCheck", "emptylineseparator", "unused", "UnusedAssignment", "InstanceVariableMayNotBeInitialized",
+        "JoinDeclarationAndAssignmentJava"})
 public class Example {
     /**
      * Returns table implementation.
@@ -240,16 +242,14 @@ public class Example {
             String bankName;
         }
 
-        KeyValueView<OrderKey, OrderValue> orderKvView = t.keyValueView(Mapper.of(OrderKey.class),
-                Mapper.builderFor(OrderValue.class)
-                        .map("billingDetails", (row) -> {
-                            BinaryObject binObj = row.binaryObjectValue("conditionalDetails");
-                            int type = row.intValue("type");
+        KeyValueView<OrderKey, OrderValue> orderKvView = t
+                .keyValueView(Mapper.of("key", OrderKey.class), Mapper.builderFor(OrderValue.class).map("billingDetails", (row) -> {
+                    BinaryObject binObj = row.binaryObjectValue("conditionalDetails");
+                    int type = row.intValue("type");
 
-                            return type == 0
-                                    ? BinaryObjects.deserialize(binObj, CreditCard.class) :
-                                    BinaryObjects.deserialize(binObj, BankAccount.class);
-                        }).build());
+                    return type == 0 ? BinaryObjects.deserialize(binObj, CreditCard.class)
+                            : BinaryObjects.deserialize(binObj, BankAccount.class);
+                }).build());
 
         OrderValue ov = orderKvView.get(new OrderKey(1, 1));
 
@@ -284,9 +284,8 @@ public class Example {
         binObj = orderRecord.billingDetails;
 
         // Manual deserialization is possible as well.
-        Object billingDetails = orderRecord.type == 0
-                ? BinaryObjects.deserialize(binObj, CreditCard.class) :
-                BinaryObjects.deserialize(binObj, BankAccount.class);
+        Object billingDetails = orderRecord.type == 0 ? BinaryObjects.deserialize(binObj, CreditCard.class)
+                : BinaryObjects.deserialize(binObj, BankAccount.class);
     }
 
     /**
@@ -352,25 +351,21 @@ public class Example {
             int department;
         }
 
-        RecordView<TruncatedRecord> truncatedView = t.recordView(
-                Mapper.builderFor(TruncatedRecord.class)
-                        .map("upgradedObject", JavaPersonV2.class).build());
+        RecordView<TruncatedRecord> truncatedView = t
+                .recordView(Mapper.builderFor(TruncatedRecord.class).map("upgradedObject", JavaPersonV2.class).build());
 
         // Or we can have a custom conditional type selection.
-        RecordView<TruncatedRecord> truncatedView2 = t.recordView(
-                Mapper.builderFor(TruncatedRecord.class)
-                        .map("upgradedObject", (row) -> {
-                            BinaryObject binObj1 = row.binaryObjectValue("upgradedObject");
-                            int dept = row.intValue("department");
-
-                            return dept == 0
-                                    ? BinaryObjects.deserialize(binObj1, JavaPerson.class) :
-                                    BinaryObjects.deserialize(binObj1, JavaPersonV2.class);
-                        }).build());
+        RecordView<TruncatedRecord> truncatedView2 = t.recordView(Mapper.builderFor(TruncatedRecord.class).map("upgradedObject", (row) -> {
+            BinaryObject binObj1 = row.binaryObjectValue("upgradedObject");
+            int dept = row.intValue("department");
+
+            return dept == 0 ? BinaryObjects.deserialize(binObj1, JavaPerson.class)
+                    : BinaryObjects.deserialize(binObj1, JavaPersonV2.class);
+        }).build());
     }
 
     /**
-     * Use case 1: a simple one. The table has the structure [ [id long] // key [name varchar, lastName varchar, decimal salary, int
+     * Use case 6: a simple one. The table has the structure [ [id long] // key [name varchar, lastName varchar, decimal salary, int
      * department] // value ] We show how to use the raw TableRow and a mapped class.
      */
     @Disabled
@@ -406,7 +401,7 @@ public class Example {
     }
 
     /**
-     * Use case 1: a simple one. The table has the structure [ [byte[]] // key [name varchar, lastName varchar, decimal salary, int
+     * Use case 7: a simple one. The table has the structure [ [byte[]] // key [name varchar, lastName varchar, decimal salary, int
      * department] // value ] We show how to use the raw TableRow and a mapped class.
      */
     @Disabled
@@ -425,8 +420,50 @@ public class Example {
 
         employeeView.put(1L, BinaryObjects.wrap(new byte[0] /* serialized Employee */));
 
-        t.keyValueView(
-                Mapper.identity(Long.class),
-                Mapper.builderFor(BinaryObject.class).deserializeTo(Employee.class).build());
+        t.keyValueView(Mapper.identity(Long.class), Mapper.of("value", Employee.class));
+    }
+
+    /**
+     * Use case 8: Here we show how to use mapper for single column case.
+     */
+    @Disabled
+    @ParameterizedTest
+    @MethodSource("tableFactory")
+    public void useCase8(Table t) {
+        new SchemaDescriptor(
+                1,
+                new Column[]{new Column("key", NativeTypes.INT64, false)},
+                new Column[]{new Column("val", NativeTypes.BYTES, true),
+        });
+
+        class UserObject {
+        }
+
+        class Employee {
+            UserObject data;
+        }
+
+        class Employee2 {
+            byte[] data;
+        }
+
+        // Class usage without a column name can work correctly only and only when each of key and value parts is single column.
+        KeyValueView<Long, Employee> v1 = t.keyValueView(Long.class, Employee.class);
+
+        KeyValueView<Long, Employee> v2 = t.keyValueView(
+                Mapper.of(Long.class), // Class usage without a column name can work correctly only and only when the key part is single column.
+                Mapper.builderFor(Employee.class).map("data", "val").build());
+
+        KeyValueView<Long, Employee> v3 = t.keyValueView(
+                Mapper.of("key", Long.class),
+                Mapper.builderFor(Employee.class).map("data", "val").build());
+
+        KeyValueView<Long, UserObject> v4 = t.keyValueView(
+                Mapper.of("key", Long.class),
+                Mapper.of("data", UserObject.class));
+
+        KeyValueView<Long, byte[]> v5 = t.keyValueView(
+                Mapper.of("key", Long.class),
+                Mapper.of("data", byte[].class));
     }
 }