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

chaokunyang pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/fury.git


The following commit(s) were added to refs/heads/main by this push:
     new cfe96802 feat(java): fury row encoder now supports implementing 
interfaces with simple value type (#2250)
cfe96802 is described below

commit cfe96802b07208e620715304ec37be930450f6b7
Author: Steven Schlansker <[email protected]>
AuthorDate: Sun May 25 20:25:02 2025 -0700

    feat(java): fury row encoder now supports implementing interfaces with 
simple value type (#2250)
    
    ## What does this PR do?
    
    You can now call `Encoders.registerLazyBeanInterface` and Fury will
    generate a bean implementation type for you.
    This should allow easy integration with Immutables library for example -
    I will be testing that over the coming days.
    
    ## Related issues
    
    Fixes #2223
    
    ## Does this PR introduce any user-facing change?
    
    New api to register interface types. Since this was not implemented
    before, there should be no breaking change.
    
    - [ ] Does this PR introduce any public API change?
    - [ ] Does this PR introduce any binary protocol compatibility change?
---
 .../java/org/apache/fury/annotation/FuryField.java |   2 +-
 .../java/org/apache/fury/builder/CodecBuilder.java |   8 +-
 .../apache/fury/builder/ObjectCodecBuilder.java    |   2 +-
 .../org/apache/fury/codegen/CodegenContext.java    |  12 +-
 .../main/java/org/apache/fury/meta/ClassDef.java   |   5 +-
 .../java/org/apache/fury/meta/ClassDefEncoder.java |   2 +-
 .../java/org/apache/fury/meta/TypeDefEncoder.java  |   2 +-
 .../main/java/org/apache/fury/reflect/TypeRef.java |   4 +
 .../org/apache/fury/resolver/ClassResolver.java    |  26 +++-
 .../org/apache/fury/resolver/FieldResolver.java    |  18 ++-
 .../apache/fury/serializer/ObjectSerializer.java   |   2 +-
 .../main/java/org/apache/fury/type/Descriptor.java | 121 ++++++++++++-------
 .../apache/fury/type/TypeResolutionContext.java    | 105 +++++++++++++++++
 .../main/java/org/apache/fury/type/TypeUtils.java  | 124 +++++++++----------
 .../java/org/apache/fury/CrossLanguageTest.java    |   2 +-
 java/fury-format/README.md                         |  12 +-
 .../fury/format/encoder/ArrayDataForEach.java      |  11 +-
 .../fury/format/encoder/ArrayEncoderBuilder.java   |   7 +-
 .../format/encoder/BaseBinaryEncoderBuilder.java   | 112 +++++++++---------
 .../fury/format/encoder/MapEncoderBuilder.java     |  14 +--
 .../fury/format/encoder/RowEncoderBuilder.java     | 131 ++++++++++++++++++---
 .../apache/fury/format/row/binary/BinaryUtils.java |   9 +-
 .../org/apache/fury/format/type/TypeInference.java |  76 +++++-------
 .../format/encoder/ImplementInterfaceTest.java     | 127 ++++++++++++++++++++
 24 files changed, 664 insertions(+), 270 deletions(-)

diff --git 
a/java/fury-core/src/main/java/org/apache/fury/annotation/FuryField.java 
b/java/fury-core/src/main/java/org/apache/fury/annotation/FuryField.java
index 00d1d250..9f3d9bdb 100644
--- a/java/fury-core/src/main/java/org/apache/fury/annotation/FuryField.java
+++ b/java/fury-core/src/main/java/org/apache/fury/annotation/FuryField.java
@@ -25,7 +25,7 @@ import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;
 
 @Retention(RetentionPolicy.RUNTIME)
-@Target(ElementType.FIELD)
+@Target({ElementType.FIELD, ElementType.METHOD})
 public @interface FuryField {
 
   /** Whether field is nullable, default false. */
diff --git 
a/java/fury-core/src/main/java/org/apache/fury/builder/CodecBuilder.java 
b/java/fury-core/src/main/java/org/apache/fury/builder/CodecBuilder.java
index d0f88062..bbda1ff0 100644
--- a/java/fury-core/src/main/java/org/apache/fury/builder/CodecBuilder.java
+++ b/java/fury-core/src/main/java/org/apache/fury/builder/CodecBuilder.java
@@ -67,7 +67,6 @@ import org.apache.fury.util.StringUtils;
 import org.apache.fury.util.function.Functions;
 import org.apache.fury.util.record.RecordComponent;
 import org.apache.fury.util.record.RecordUtils;
-import sun.misc.Unsafe;
 
 /**
  * Base builder for generating code to serialize java bean in row-format or 
object stream format.
@@ -93,6 +92,7 @@ public abstract class CodecBuilder {
   protected final TypeRef<?> beanType;
   protected final Class<?> beanClass;
   protected final boolean isRecord;
+  protected final boolean isInterface;
   private final Set<String> duplicatedFields;
   protected Reference furyRef = new Reference(FURY_NAME, 
TypeRef.of(Fury.class));
   public static final Reference recordComponentDefaultValues =
@@ -105,10 +105,11 @@ public abstract class CodecBuilder {
     this.beanType = beanType;
     this.beanClass = getRawType(beanType);
     isRecord = RecordUtils.isRecord(beanClass);
+    isInterface = beanClass.isInterface();
     if (isRecord) {
       recordCtrAccessible = recordCtrAccessible(beanClass);
     }
-    duplicatedFields = 
Descriptor.getSortedDuplicatedFields(beanClass).keySet();
+    duplicatedFields = 
Descriptor.getSortedDuplicatedMembers(beanClass).keySet();
     // don't ctx.addImport beanClass, because it maybe causes name collide.
     ctx.reserveName(FURY_NAME);
     ctx.reserveName(ROOT_OBJECT_NAME);
@@ -191,6 +192,9 @@ public abstract class CodecBuilder {
     TypeRef<?> fieldType = descriptor.getTypeRef();
     Class<?> rawType = descriptor.getRawType();
     String fieldName = descriptor.getName();
+    if (isInterface) {
+      return new Invoke(inputBeanExpr, descriptor.getName(), fieldName, 
fieldType);
+    }
     if (isRecord) {
       return getRecordFieldValue(inputBeanExpr, descriptor);
     }
diff --git 
a/java/fury-core/src/main/java/org/apache/fury/builder/ObjectCodecBuilder.java 
b/java/fury-core/src/main/java/org/apache/fury/builder/ObjectCodecBuilder.java
index 3429512b..eb0a837f 100644
--- 
a/java/fury-core/src/main/java/org/apache/fury/builder/ObjectCodecBuilder.java
+++ 
b/java/fury-core/src/main/java/org/apache/fury/builder/ObjectCodecBuilder.java
@@ -96,7 +96,7 @@ public class ObjectCodecBuilder extends 
BaseObjectCodecBuilder {
                       .getClassDef(beanClass, true)
                       .getDescriptors(classResolver, beanClass));
     } else {
-      descriptors = fury.getClassResolver().getAllDescriptorsMap(beanClass, 
true).values();
+      descriptors = fury.getClassResolver().getFieldDescriptors(beanClass, 
true);
     }
     DescriptorGrouper grouper = 
classResolver.createDescriptorGrouper(descriptors, false);
     descriptors = grouper.getSortedDescriptors();
diff --git 
a/java/fury-core/src/main/java/org/apache/fury/codegen/CodegenContext.java 
b/java/fury-core/src/main/java/org/apache/fury/codegen/CodegenContext.java
index d6568d46..203df52c 100644
--- a/java/fury-core/src/main/java/org/apache/fury/codegen/CodegenContext.java
+++ b/java/fury-core/src/main/java/org/apache/fury/codegen/CodegenContext.java
@@ -140,6 +140,7 @@ public class CodegenContext {
   String pkg;
   LinkedHashSet<String> imports = new LinkedHashSet<>();
   String className;
+  String classModifiers = "public final";
   String[] superClasses;
   String[] interfaces;
   List<FieldInfo> fields = new ArrayList<>();
@@ -415,6 +416,15 @@ public class CodegenContext {
     this.className = className;
   }
 
+  /**
+   * Set class modifiers. Default is {@code public final}.
+   *
+   * @param classModifiers the new class modifiers
+   */
+  public void setClassModifiers(String classModifiers) {
+    this.classModifiers = classModifiers;
+  }
+
   /**
    * Set super classes.
    *
@@ -626,7 +636,7 @@ public class CodegenContext {
       codeBuilder.append('\n');
     }
 
-    codeBuilder.append(String.format("public final class %s ", className));
+    codeBuilder.append(String.format("%s class %s ", classModifiers, 
className));
     if (superClasses != null) {
       codeBuilder.append(String.format("extends %s ", String.join(", ", 
superClasses)));
     }
diff --git a/java/fury-core/src/main/java/org/apache/fury/meta/ClassDef.java 
b/java/fury-core/src/main/java/org/apache/fury/meta/ClassDef.java
index 1d330e6a..a2f56b41 100644
--- a/java/fury-core/src/main/java/org/apache/fury/meta/ClassDef.java
+++ b/java/fury-core/src/main/java/org/apache/fury/meta/ClassDef.java
@@ -30,6 +30,7 @@ import java.io.ObjectStreamClass;
 import java.io.Serializable;
 import java.lang.reflect.Array;
 import java.lang.reflect.Field;
+import java.lang.reflect.Member;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -240,9 +241,9 @@ public class ClassDef implements Serializable {
    */
   public List<Descriptor> getDescriptors(ClassResolver resolver, Class<?> cls) 
{
     if (descriptors == null) {
-      SortedMap<Field, Descriptor> allDescriptorsMap = 
resolver.getAllDescriptorsMap(cls, true);
+      SortedMap<Member, Descriptor> allDescriptorsMap = 
resolver.getAllDescriptorsMap(cls, true);
       Map<String, Descriptor> descriptorsMap = new HashMap<>();
-      for (Map.Entry<Field, Descriptor> e : allDescriptorsMap.entrySet()) {
+      for (Map.Entry<Member, Descriptor> e : allDescriptorsMap.entrySet()) {
         if (descriptorsMap.put(
                 e.getKey().getDeclaringClass().getName() + "." + 
e.getKey().getName(), e.getValue())
             != null) {
diff --git 
a/java/fury-core/src/main/java/org/apache/fury/meta/ClassDefEncoder.java 
b/java/fury-core/src/main/java/org/apache/fury/meta/ClassDefEncoder.java
index e0f43759..6ddf56e3 100644
--- a/java/fury-core/src/main/java/org/apache/fury/meta/ClassDefEncoder.java
+++ b/java/fury-core/src/main/java/org/apache/fury/meta/ClassDefEncoder.java
@@ -59,7 +59,7 @@ class ClassDefEncoder {
     DescriptorGrouper descriptorGrouper =
         fury.getClassResolver()
             .createDescriptorGrouper(
-                fury.getClassResolver().getAllDescriptorsMap(cls, 
resolveParent).values(),
+                fury.getClassResolver().getFieldDescriptors(cls, 
resolveParent),
                 false,
                 Function.identity());
     List<Field> fields = new ArrayList<>();
diff --git 
a/java/fury-core/src/main/java/org/apache/fury/meta/TypeDefEncoder.java 
b/java/fury-core/src/main/java/org/apache/fury/meta/TypeDefEncoder.java
index d50c1452..f828a206 100644
--- a/java/fury-core/src/main/java/org/apache/fury/meta/TypeDefEncoder.java
+++ b/java/fury-core/src/main/java/org/apache/fury/meta/TypeDefEncoder.java
@@ -55,7 +55,7 @@ class TypeDefEncoder {
     DescriptorGrouper descriptorGrouper =
         fury.getClassResolver()
             .createDescriptorGrouper(
-                fury.getClassResolver().getAllDescriptorsMap(type, 
true).values(),
+                fury.getClassResolver().getFieldDescriptors(type, true),
                 false,
                 Function.identity());
     List<Field> fields =
diff --git a/java/fury-core/src/main/java/org/apache/fury/reflect/TypeRef.java 
b/java/fury-core/src/main/java/org/apache/fury/reflect/TypeRef.java
index c521096d..eb06df10 100644
--- a/java/fury-core/src/main/java/org/apache/fury/reflect/TypeRef.java
+++ b/java/fury-core/src/main/java/org/apache/fury/reflect/TypeRef.java
@@ -167,6 +167,10 @@ public class TypeRef<T> {
     return type instanceof Class && ((Class<?>) type).isPrimitive();
   }
 
+  public boolean isInterface() {
+    return getRawType().isInterface();
+  }
+
   /**
    * Returns true if this type is known to be an array type, such as {@code 
int[]}, {@code T[]},
    * {@code <? extends Map<String, Integer>[]>} etc.
diff --git 
a/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java 
b/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java
index 930a086b..f4661179 100644
--- a/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java
+++ b/java/fury-core/src/main/java/org/apache/fury/resolver/ClassResolver.java
@@ -38,6 +38,7 @@ import java.io.Externalizable;
 import java.io.IOException;
 import java.io.Serializable;
 import java.lang.reflect.Field;
+import java.lang.reflect.Member;
 import java.lang.reflect.Type;
 import java.math.BigDecimal;
 import java.math.BigInteger;
@@ -262,7 +263,7 @@ public class ClassResolver implements TypeResolver {
     // Tuple2<Class, Class>: Tuple2<From Class, To Class>
     private final Map<Tuple2<Class<?>, Class<?>>, ClassInfo> 
transformedClassInfo = new HashMap<>();
     // TODO(chaokunyang) Better to  use soft reference, see ObjectStreamClass.
-    private final ConcurrentHashMap<Tuple2<Class<?>, Boolean>, 
SortedMap<Field, Descriptor>>
+    private final ConcurrentHashMap<Tuple2<Class<?>, Boolean>, 
SortedMap<Member, Descriptor>>
         descriptorsCache = new ConcurrentHashMap<>();
     private ClassChecker classChecker = (classResolver, className) -> true;
     private GenericType objectGenericType;
@@ -873,6 +874,7 @@ public class ClassResolver implements TypeResolver {
   }
 
   /** Get or create serializer for <code>cls</code>. */
+  @Override
   @SuppressWarnings("unchecked")
   public <T> Serializer<T> getSerializer(Class<T> cls) {
     Preconditions.checkNotNull(cls);
@@ -1187,8 +1189,20 @@ public class ClassResolver implements TypeResolver {
     return fieldResolver;
   }
 
+  public List<Descriptor> getFieldDescriptors(Class<?> clz, boolean 
searchParent) {
+    SortedMap<Member, Descriptor> allDescriptors = getAllDescriptorsMap(clz, 
searchParent);
+    List<Descriptor> result = new ArrayList<>(allDescriptors.size());
+    allDescriptors.forEach(
+        (member, descriptor) -> {
+          if (member instanceof Field) {
+            result.add(descriptor);
+          }
+        });
+    return result;
+  }
+
   // thread safe
-  public SortedMap<Field, Descriptor> getAllDescriptorsMap(Class<?> clz, 
boolean searchParent) {
+  public SortedMap<Member, Descriptor> getAllDescriptorsMap(Class<?> clz, 
boolean searchParent) {
     // when jit thread query this, it is already built by serialization main 
thread.
     return extRegistry.descriptorsCache.computeIfAbsent(
         Tuple2.of(clz, searchParent), t -> 
Descriptor.getAllDescriptorsMap(clz, searchParent));
@@ -1198,6 +1212,7 @@ public class ClassResolver implements TypeResolver {
    * Whether to track reference for this type. If false, reference tracing of 
subclasses may be
    * ignored too.
    */
+  @Override
   public boolean needToWriteRef(TypeRef<?> typeRef) {
     Object extInfo = typeRef.getExtInfo();
     if (extInfo instanceof TypeExtMeta) {
@@ -1239,6 +1254,7 @@ public class ClassResolver implements TypeResolver {
   }
 
   /** Get classinfo by cache, update cache if miss. */
+  @Override
   public ClassInfo getClassInfo(Class<?> cls, ClassInfoHolder classInfoHolder) 
{
     ClassInfo classInfo = classInfoHolder.classInfo;
     if (classInfo.getCls() != cls) {
@@ -1439,6 +1455,7 @@ public class ClassResolver implements TypeResolver {
   // }
 
   /** Write classname for java serialization. */
+  @Override
   public void writeClassInfo(MemoryBuffer buffer, ClassInfo classInfo) {
     if (metaContextShareEnabled) {
       // FIXME(chaokunyang) Register class but not register serializer can't 
be used with
@@ -1835,6 +1852,7 @@ public class ClassResolver implements TypeResolver {
   }
 
   /** Read class info, update classInfoHolder if cache not hit. */
+  @Override
   @CodegenInvoke
   public ClassInfo readClassInfo(MemoryBuffer buffer, ClassInfoHolder 
classInfoHolder) {
     if (metaContextShareEnabled) {
@@ -1986,6 +2004,7 @@ public class ClassResolver implements TypeResolver {
 
   public void resetWrite() {}
 
+  @Override
   public GenericType buildGenericType(TypeRef<?> typeRef) {
     return GenericType.build(
         typeRef,
@@ -1998,6 +2017,7 @@ public class ClassResolver implements TypeResolver {
         });
   }
 
+  @Override
   public GenericType buildGenericType(Type type) {
     GenericType genericType = extRegistry.genericTypes.get(type);
     if (genericType != null) {
@@ -2030,10 +2050,12 @@ public class ClassResolver implements TypeResolver {
   }
 
   // Invoked by fury JIT.
+  @Override
   public ClassInfo nilClassInfo() {
     return new ClassInfo(this, null, null, NO_CLASS_ID, NOT_SUPPORT_XLANG);
   }
 
+  @Override
   public ClassInfoHolder nilClassInfoHolder() {
     return new ClassInfoHolder(nilClassInfo());
   }
diff --git 
a/java/fury-core/src/main/java/org/apache/fury/resolver/FieldResolver.java 
b/java/fury-core/src/main/java/org/apache/fury/resolver/FieldResolver.java
index 35337d5f..396bebb5 100644
--- a/java/fury-core/src/main/java/org/apache/fury/resolver/FieldResolver.java
+++ b/java/fury-core/src/main/java/org/apache/fury/resolver/FieldResolver.java
@@ -28,6 +28,7 @@ import static 
org.apache.fury.resolver.FieldResolver.FieldInfoEncodingType.SEPAR
 import static org.apache.fury.type.TypeUtils.getRawType;
 
 import java.lang.reflect.Field;
+import java.lang.reflect.Member;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -42,6 +43,7 @@ import java.util.SortedMap;
 import java.util.SortedSet;
 import java.util.TreeSet;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.apache.fury.Fury;
 import org.apache.fury.annotation.FuryField;
 import org.apache.fury.collection.Tuple2;
@@ -194,19 +196,29 @@ public class FieldResolver {
   public static FieldResolver of(
       Fury fury, Class<?> type, boolean resolveParent, boolean 
ignoreCollectionType) {
     // all fields of class and super classes should be a consistent order 
between jvm process.
-    SortedMap<Field, Descriptor> allFieldsMap =
+    SortedMap<Member, Descriptor> allFieldsMap =
         fury.getClassResolver().getAllDescriptorsMap(type, resolveParent);
     Set<String> duplicatedFields;
     if (resolveParent) {
-      duplicatedFields = Descriptor.getSortedDuplicatedFields(type).keySet();
+      duplicatedFields = Descriptor.getSortedDuplicatedMembers(type).keySet();
     } else {
       duplicatedFields = new HashSet<>();
     }
     List<ClassField> allFields =
-        
allFieldsMap.keySet().stream().map(ClassField::new).collect(Collectors.toList());
+        allFieldsMap.keySet().stream()
+            .flatMap(FieldResolver::mapField)
+            .map(ClassField::new)
+            .collect(Collectors.toList());
     return new FieldResolver(fury, type, ignoreCollectionType, allFields, 
duplicatedFields);
   }
 
+  private static Stream<Field> mapField(Member member) {
+    if (member instanceof Field) {
+      return Stream.of((Field) member);
+    }
+    return Stream.empty();
+  }
+
   private final Class<?> cls;
   private final Fury fury;
   private final RefResolver refResolver;
diff --git 
a/java/fury-core/src/main/java/org/apache/fury/serializer/ObjectSerializer.java 
b/java/fury-core/src/main/java/org/apache/fury/serializer/ObjectSerializer.java
index 82e4cae7..3c131ca5 100644
--- 
a/java/fury-core/src/main/java/org/apache/fury/serializer/ObjectSerializer.java
+++ 
b/java/fury-core/src/main/java/org/apache/fury/serializer/ObjectSerializer.java
@@ -98,7 +98,7 @@ public final class ObjectSerializer<T> extends 
AbstractObjectSerializer<T> {
       ClassDef classDef = classResolver.getClassDef(cls, resolveParent);
       descriptors = classDef.getDescriptors(classResolver, cls);
     } else {
-      descriptors = fury.getClassResolver().getAllDescriptorsMap(cls, 
resolveParent).values();
+      descriptors = fury.getClassResolver().getFieldDescriptors(cls, 
resolveParent);
     }
     DescriptorGrouper descriptorGrouper = 
classResolver.createDescriptorGrouper(descriptors, false);
     descriptors = descriptorGrouper.getSortedDescriptors();
diff --git a/java/fury-core/src/main/java/org/apache/fury/type/Descriptor.java 
b/java/fury-core/src/main/java/org/apache/fury/type/Descriptor.java
index 05cd7a33..9d21c46d 100644
--- a/java/fury-core/src/main/java/org/apache/fury/type/Descriptor.java
+++ b/java/fury-core/src/main/java/org/apache/fury/type/Descriptor.java
@@ -24,6 +24,7 @@ import static 
org.apache.fury.util.Preconditions.checkArgument;
 import com.google.common.cache.Cache;
 import com.google.common.cache.CacheBuilder;
 import java.lang.reflect.Field;
+import java.lang.reflect.Member;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
 import java.util.ArrayList;
@@ -37,6 +38,7 @@ import java.util.Objects;
 import java.util.Set;
 import java.util.SortedMap;
 import java.util.TreeMap;
+import java.util.TreeSet;
 import java.util.WeakHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -60,7 +62,8 @@ import org.apache.fury.util.record.RecordUtils;
  * @see Ignore
  */
 public class Descriptor {
-  private static Cache<Class<?>, Tuple2<SortedMap<Field, Descriptor>, 
SortedMap<Field, Descriptor>>>
+  private static Cache<
+          Class<?>, Tuple2<SortedMap<Member, Descriptor>, SortedMap<Member, 
Descriptor>>>
       descCache = 
CacheBuilder.newBuilder().weakKeys().softValues().concurrencyLevel(64).build();
   private static final Map<Class<?>, AtomicBoolean> flags =
       Collections.synchronizedMap(new WeakHashMap<>());
@@ -132,6 +135,21 @@ public class Descriptor {
     }
   }
 
+  private Descriptor(Method readMethod) {
+    this.field = null;
+    this.typeName = readMethod.getReturnType().getName();
+    this.name = readMethod.getName();
+    this.modifier = readMethod.getModifiers();
+    this.declaringClass = readMethod.getDeclaringClass().getName();
+    this.readMethod = readMethod;
+    this.writeMethod = null;
+    this.typeRef = TypeRef.of(readMethod.getGenericReturnType());
+    this.furyField = readMethod.getAnnotation(FuryField.class);
+    if (!readMethod.getReturnType().isPrimitive()) {
+      this.nullable = furyField == null || furyField.nullable();
+    }
+  }
+
   private Descriptor(
       TypeRef<?> typeRef,
       String typeName,
@@ -292,8 +310,8 @@ public class Descriptor {
    */
   public static List<Descriptor> getDescriptors(Class<?> clz) {
     // TODO(chaokunyang) add cache by weak class key, see 
java.io.ObjectStreamClass.WeakClassKey.
-    SortedMap<Field, Descriptor> allDescriptorsMap = getAllDescriptorsMap(clz);
-    Map<String, List<Field>> duplicateNameFields = 
getDuplicateNameFields(allDescriptorsMap);
+    SortedMap<Member, Descriptor> allDescriptorsMap = 
getAllDescriptorsMap(clz);
+    Map<String, List<Member>> duplicateNameFields = 
getDuplicateNames(allDescriptorsMap);
     checkArgument(
         duplicateNameFields.size() == 0, "%s has duplicate fields %s", clz, 
duplicateNameFields);
     return new ArrayList<>(allDescriptorsMap.values());
@@ -304,8 +322,8 @@ public class Descriptor {
    * not allowed to have duplicate name field.
    */
   public static SortedMap<String, Descriptor> getDescriptorsMap(Class<?> clz) {
-    SortedMap<Field, Descriptor> allDescriptorsMap = getAllDescriptorsMap(clz);
-    Map<String, List<Field>> duplicateNameFields = 
getDuplicateNameFields(allDescriptorsMap);
+    SortedMap<Member, Descriptor> allDescriptorsMap = 
getAllDescriptorsMap(clz);
+    Map<String, List<Member>> duplicateNameFields = 
getDuplicateNames(allDescriptorsMap);
     Preconditions.checkArgument(
         duplicateNameFields.size() == 0, "%s has duplicate fields %s", clz, 
duplicateNameFields);
     TreeMap<String, Descriptor> map = new TreeMap<>();
@@ -313,14 +331,14 @@ public class Descriptor {
     return map;
   }
 
-  private static final ClassValue<Map<String, List<Field>>> 
sortedDuplicatedFields =
-      new ClassValue<Map<String, List<Field>>>() {
+  private static final ClassValue<Map<String, List<Member>>> 
sortedDuplicatedMembers =
+      new ClassValue<Map<String, List<Member>>>() {
         @Override
-        protected Map<String, List<Field>> computeValue(Class<?> type) {
-          SortedMap<Field, Descriptor> allFields = 
Descriptor.getAllDescriptorsMap(type);
-          Map<String, List<Field>> duplicated = 
Descriptor.getDuplicateNameFields(allFields);
-          Map<String, List<Field>> map = new HashMap<>();
-          for (Map.Entry<String, List<Field>> e : duplicated.entrySet()) {
+        protected Map<String, List<Member>> computeValue(Class<?> type) {
+          SortedMap<Member, Descriptor> allFields = 
Descriptor.getAllDescriptorsMap(type);
+          Map<String, List<Member>> duplicated = 
Descriptor.getDuplicateNames(allFields);
+          Map<String, List<Member>> map = new HashMap<>();
+          for (Map.Entry<String, List<Member>> e : duplicated.entrySet()) {
             e.getValue()
                 .sort(
                     (f1, f2) -> {
@@ -340,36 +358,35 @@ public class Descriptor {
         }
       };
 
-  public static Map<String, List<Field>> getDuplicateNameFields(
-      SortedMap<Field, Descriptor> allDescriptorsMap) {
-    Map<String, List<Field>> duplicateNameFields = new HashMap<>();
-    for (Field field : allDescriptorsMap.keySet()) {
-      duplicateNameFields.compute(
-          field.getName(),
-          (fieldName, fields) -> {
-            if (fields == null) {
-              fields = new ArrayList<>();
+  public static Map<String, List<Member>> getDuplicateNames(
+      SortedMap<Member, Descriptor> allDescriptorsMap) {
+    Map<String, List<Member>> duplicateNames = new HashMap<>();
+    for (Member member : allDescriptorsMap.keySet()) {
+      duplicateNames.compute(
+          member.getName(),
+          (memberName, members) -> {
+            if (members == null) {
+              members = new ArrayList<>();
             }
-            fields.add(field);
-            return fields;
+            members.add(member);
+            return members;
           });
     }
-    Map<String, List<Field>> map = new HashMap<>();
-    for (Map.Entry<String, List<Field>> e : duplicateNameFields.entrySet()) {
+    Map<String, List<Member>> map = new HashMap<>();
+    for (Map.Entry<String, List<Member>> e : duplicateNames.entrySet()) {
       if (Objects.requireNonNull(e.getValue()).size() > 1) {
         map.put(e.getKey(), e.getValue());
       }
     }
-    duplicateNameFields = map;
-    return duplicateNameFields;
+    return map;
   }
 
-  public static Map<String, List<Field>> getSortedDuplicatedFields(Class<?> 
cls) {
-    return sortedDuplicatedFields.get(cls);
+  public static Map<String, List<Member>> getSortedDuplicatedMembers(Class<?> 
cls) {
+    return sortedDuplicatedMembers.get(cls);
   }
 
   public static boolean hasDuplicateNameFields(Class<?> clz) {
-    return !getSortedDuplicatedFields(clz).isEmpty();
+    return !getSortedDuplicatedMembers(clz).isEmpty();
   }
 
   /**
@@ -377,7 +394,13 @@ public class Descriptor {
    * name first and declaring class second. Super class and sub class can have 
same name field.
    */
   public static Set<Field> getFields(Class<?> clz) {
-    return getAllDescriptorsMap(clz).keySet();
+    Set<Field> fields = new TreeSet<>(memberComparator);
+    for (Member member : getAllDescriptorsMap(clz).keySet()) {
+      if (member instanceof Field) {
+        fields.add((Field) member);
+      }
+    }
+    return fields;
   }
 
   /**
@@ -385,24 +408,24 @@ public class Descriptor {
    * field name first and declaring class second. Super class and subclass can 
have same names
    * field.
    */
-  public static SortedMap<Field, Descriptor> getAllDescriptorsMap(Class<?> 
clz) {
+  public static SortedMap<Member, Descriptor> getAllDescriptorsMap(Class<?> 
clz) {
     return getAllDescriptorsMap(clz, true);
   }
 
-  private static final Comparator<Field> fieldComparator =
-      ((Field f1, Field f2) -> {
-        int compare = f1.getName().compareTo(f2.getName());
+  private static final Comparator<Member> memberComparator =
+      ((Member m1, Member m2) -> {
+        int compare = m1.getName().compareTo(m2.getName());
         if (compare == 0) { // class and super classes have same named field
-          return 
f1.getDeclaringClass().getName().compareTo(f2.getDeclaringClass().getName());
+          return 
m1.getDeclaringClass().getName().compareTo(m2.getDeclaringClass().getName());
         } else {
           return compare;
         }
       });
 
-  public static SortedMap<Field, Descriptor> getAllDescriptorsMap(
+  public static SortedMap<Member, Descriptor> getAllDescriptorsMap(
       Class<?> clz, boolean searchParent) {
     try {
-      Tuple2<SortedMap<Field, Descriptor>, SortedMap<Field, Descriptor>> 
tuple2 =
+      Tuple2<SortedMap<Member, Descriptor>, SortedMap<Member, Descriptor>> 
tuple2 =
           descCache.get(clz, () -> createAllDescriptorsMap(clz));
       if (searchParent) {
         return tuple2.f0;
@@ -414,11 +437,11 @@ public class Descriptor {
     }
   }
 
-  private static Tuple2<SortedMap<Field, Descriptor>, SortedMap<Field, 
Descriptor>>
+  private static Tuple2<SortedMap<Member, Descriptor>, SortedMap<Member, 
Descriptor>>
       createAllDescriptorsMap(Class<?> clz) {
     // use TreeMap to sort to fix field order
-    TreeMap<Field, Descriptor> descriptorMap = new TreeMap<>(fieldComparator);
-    TreeMap<Field, Descriptor> currentDescriptorMap = new 
TreeMap<>(fieldComparator);
+    TreeMap<Member, Descriptor> descriptorMap = new 
TreeMap<>(memberComparator);
+    TreeMap<Member, Descriptor> currentDescriptorMap = new 
TreeMap<>(memberComparator);
     Class<?> clazz = clz;
     // TODO(chaokunyang) use fury compiler thread pool
     ExecutorService compilationService = ForkJoinPool.commonPool();
@@ -437,6 +460,16 @@ public class Descriptor {
       currentDescriptorMap = new TreeMap<>(descriptorMap);
       return Tuple2.of(descriptorMap, currentDescriptorMap);
     }
+    if (clazz.isInterface()) {
+      for (Method method : clazz.getMethods()) {
+        if (method.getParameterCount() == 0 && method.getReturnType() != 
void.class) {
+          descriptorMap.put(method, new Descriptor(method));
+        }
+      }
+
+      currentDescriptorMap = new TreeMap<>(descriptorMap);
+      return Tuple2.of(descriptorMap, currentDescriptorMap);
+    }
     do {
       Field[] fields = clazz.getDeclaredFields();
       boolean haveExpose = false, haveIgnore = false;
@@ -542,7 +575,7 @@ public class Descriptor {
       Class<?> clz, boolean searchParent) {
     List<Field> fieldList = new ArrayList<>();
     Class<?> clazz = clz;
-    Map<Tuple2<Class, String>, Method> methodMap = new HashMap<>();
+    Map<Tuple2<Class<?>, String>, Method> methodMap = new HashMap<>();
     do {
       Field[] fields = clazz.getDeclaredFields();
       for (Field field : fields) {
@@ -571,7 +604,7 @@ public class Descriptor {
     }
 
     // use TreeMap to sort to fix field order
-    TreeMap<Field, Descriptor> descriptorMap = new TreeMap<>(fieldComparator);
+    TreeMap<Field, Descriptor> descriptorMap = new TreeMap<>(memberComparator);
     for (Field field : fieldList) {
       Class<?> fieldDeclaringClass = field.getDeclaringClass();
       String fieldName = field.getName();
@@ -601,7 +634,7 @@ public class Descriptor {
           setter = null;
         }
       }
-      TypeRef fieldType = TypeRef.of(field.getGenericType());
+      TypeRef<?> fieldType = TypeRef.of(field.getGenericType());
       descriptorMap.put(field, new Descriptor(field, fieldType, getter, 
setter));
     }
     // Don't cache descriptors using a static `WeakHashMap<Class<?>, 
SortedMap<Field, Descriptor>>`,
diff --git 
a/java/fury-core/src/main/java/org/apache/fury/type/TypeResolutionContext.java 
b/java/fury-core/src/main/java/org/apache/fury/type/TypeResolutionContext.java
new file mode 100644
index 00000000..aa5d1506
--- /dev/null
+++ 
b/java/fury-core/src/main/java/org/apache/fury/type/TypeResolutionContext.java
@@ -0,0 +1,105 @@
+/*
+ * 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.fury.type;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import org.apache.fury.annotation.Internal;
+import org.apache.fury.reflect.TypeRef;
+
+@Internal
+public class TypeResolutionContext {
+  private final CustomTypeRegistry customTypeRegistry;
+  private final LinkedHashSet<TypeRef<?>> walkedTypePath;
+  private final Set<Class<?>> synthesizedBeanTypes;
+
+  public TypeResolutionContext(CustomTypeRegistry customTypeRegistry) {
+    this.customTypeRegistry = customTypeRegistry;
+    walkedTypePath = new LinkedHashSet<>();
+    synthesizedBeanTypes = Collections.emptySet();
+  }
+
+  public TypeResolutionContext(
+      CustomTypeRegistry customTypeRegistry,
+      LinkedHashSet<TypeRef<?>> walkedTypePath,
+      Set<Class<?>> synthesizedBeanTypes) {
+    this.customTypeRegistry = customTypeRegistry;
+    this.walkedTypePath = walkedTypePath;
+    this.synthesizedBeanTypes = synthesizedBeanTypes;
+  }
+
+  public CustomTypeRegistry getCustomTypeRegistry() {
+    return customTypeRegistry;
+  }
+
+  public LinkedHashSet<TypeRef<?>> getWalkedTypePath() {
+    return walkedTypePath;
+  }
+
+  public Set<Class<?>> getSynthesizedBeanTypes() {
+    return synthesizedBeanTypes;
+  }
+
+  public TypeRef<?> getEnclosingType() {
+    TypeRef<?> result = TypeRef.of(Object.class);
+    for (TypeRef<?> type : walkedTypePath) { // .getLast()
+      result = type;
+    }
+    return result;
+  }
+
+  public TypeResolutionContext appendTypePath(TypeRef<?>... typeRef) {
+    LinkedHashSet<TypeRef<?>> newWalkedTypePath = new 
LinkedHashSet<>(walkedTypePath);
+    newWalkedTypePath.addAll(Arrays.asList(typeRef));
+    return new TypeResolutionContext(customTypeRegistry, newWalkedTypePath, 
synthesizedBeanTypes);
+  }
+
+  public TypeResolutionContext appendTypePath(Class<?> clz) {
+    return appendTypePath(TypeRef.of(clz));
+  }
+
+  public TypeResolutionContext withSynthesizedBeanType(Class<?> clz) {
+    Set<Class<?>> newSynthesizedBeanTypes = new 
HashSet<>(synthesizedBeanTypes);
+    newSynthesizedBeanTypes.add(clz);
+    return new TypeResolutionContext(customTypeRegistry, walkedTypePath, 
newSynthesizedBeanTypes);
+  }
+
+  public boolean isSynthesizedBeanType(Class<?> cls) {
+    return synthesizedBeanTypes.contains(cls);
+  }
+
+  public void checkNoCycle(Class<?> clz) {
+    checkNoCycle(TypeRef.of(clz));
+  }
+
+  public void checkNoCycle(TypeRef<?> typeRef) {
+    if (walkedTypePath.contains(typeRef)
+        || walkedTypePath.contains(TypeRef.of(typeRef.getRawType()))) {
+      throw new UnsupportedOperationException(
+          "cyclic type is not supported. walkedTypePath: "
+              + walkedTypePath
+              + " seen type: "
+              + typeRef);
+    }
+  }
+}
diff --git a/java/fury-core/src/main/java/org/apache/fury/type/TypeUtils.java 
b/java/fury-core/src/main/java/org/apache/fury/type/TypeUtils.java
index 2226128f..3262f7fd 100644
--- a/java/fury-core/src/main/java/org/apache/fury/type/TypeUtils.java
+++ b/java/fury-core/src/main/java/org/apache/fury/type/TypeUtils.java
@@ -336,7 +336,7 @@ public class TypeUtils {
     if (type.getClass() == Class.class) {
       return (Class<?>) type;
     } else {
-      return getRawType(typeRef.getType());
+      return getRawType(type);
     }
   }
 
@@ -549,6 +549,10 @@ public class TypeUtils {
         .where(new TypeParameter<V>() {}, valueType);
   }
 
+  public static boolean isContainer(Class<?> cls) {
+    return isCollection(cls) || isMap(cls) || cls.isArray();
+  }
+
   public static boolean isCollection(Class<?> cls) {
     return cls == ArrayList.class || Collection.class.isAssignableFrom(cls);
   }
@@ -569,7 +573,11 @@ public class TypeUtils {
   }
 
   public static boolean isBean(Type type, CustomTypeRegistry customTypes) {
-    return isBean(TypeRef.of(type), new LinkedHashSet<>(), customTypes);
+    return isBean(TypeRef.of(type), customTypes);
+  }
+
+  public static boolean isBean(TypeRef<?> type, CustomTypeRegistry 
customTypes) {
+    return isBean(type, new TypeResolutionContext(customTypes));
   }
 
   /**
@@ -578,18 +586,18 @@ public class TypeUtils {
    * nested class is ok.
    */
   public static boolean isBean(TypeRef<?> typeRef) {
-    return isBean(typeRef, new LinkedHashSet<>());
+    return isBean(typeRef, new 
TypeResolutionContext(CustomTypeRegistry.EMPTY));
   }
 
-  private static boolean isBean(TypeRef<?> typeRef, LinkedHashSet<TypeRef<?>> 
walkedTypePath) {
-    return isBean(typeRef, walkedTypePath, CustomTypeRegistry.EMPTY);
+  public static boolean isBean(Class<?> clz, TypeResolutionContext ctx) {
+    return isBean(TypeRef.of(clz), ctx);
   }
 
-  private static boolean isBean(
-      TypeRef<?> typeRef,
-      LinkedHashSet<TypeRef<?>> walkedTypePath,
-      CustomTypeRegistry customTypes) {
+  public static boolean isBean(TypeRef<?> typeRef, TypeResolutionContext ctx) {
     Class<?> cls = getRawType(typeRef);
+    if (ctx.isSynthesizedBeanType(cls)) {
+      return true;
+    }
     if (Modifier.isAbstract(cls.getModifiers()) || 
Modifier.isInterface(cls.getModifiers())) {
       return false;
     }
@@ -600,8 +608,7 @@ public class TypeUtils {
       if (cls.getEnclosingClass() != null && 
!Modifier.isStatic(cls.getModifiers())) {
         return false;
       }
-      LinkedHashSet<TypeRef<?>> newTypePath = new 
LinkedHashSet<>(walkedTypePath);
-      newTypePath.add(typeRef);
+      TypeResolutionContext newTypePath = ctx.appendTypePath(typeRef);
       if (cls == Object.class) {
         // return false for typeToken that point to un-specialized generic 
type.
         return false;
@@ -613,7 +620,6 @@ public class TypeUtils {
               && !ITERABLE_TYPE.isSupertypeOf(typeRef)
               && !MAP_TYPE.isSupertypeOf(typeRef);
       if (maybe) {
-        Class<?> enclosingType = enclosingType(newTypePath);
         return Descriptor.getDescriptors(cls).stream()
             .allMatch(
                 d -> {
@@ -621,9 +627,9 @@ public class TypeUtils {
                   // do field modifiers and getter/setter validation here, not 
in getDescriptors.
                   // If Modifier.isFinal(d.getModifiers()), use reflection
                   // private field that doesn't have getter/setter will be 
handled by reflection.
-                  return customTypes.hasCodec(enclosingType, t.getRawType())
-                      || isSupported(t, newTypePath, customTypes)
-                      || isBean(t, newTypePath, customTypes);
+                  return ctx.getCustomTypeRegistry().hasCodec(cls, 
t.getRawType())
+                      || isSupported(t, newTypePath)
+                      || isBean(t, newTypePath);
                 });
       } else {
         return false;
@@ -635,13 +641,10 @@ public class TypeUtils {
 
   /** Check if <code>typeToken</code> is supported by row-format. */
   public static boolean isSupported(TypeRef<?> typeRef) {
-    return isSupported(typeRef, new LinkedHashSet<>(), 
CustomTypeRegistry.EMPTY);
+    return isSupported(typeRef, new 
TypeResolutionContext(CustomTypeRegistry.EMPTY));
   }
 
-  private static boolean isSupported(
-      TypeRef<?> typeRef,
-      LinkedHashSet<TypeRef<?>> walkedTypePath,
-      CustomTypeRegistry customTypes) {
+  private static boolean isSupported(TypeRef<?> typeRef, TypeResolutionContext 
ctx) {
     Class<?> cls = getRawType(typeRef);
     if (!Modifier.isPublic(cls.getModifiers())) {
       return false;
@@ -657,7 +660,8 @@ public class TypeUtils {
       return isSupported(Objects.requireNonNull(typeRef.getComponentType()));
     } else if (ITERABLE_TYPE.isSupertypeOf(typeRef)) {
       TypeRef<?> elementType = getElementType(typeRef);
-      if (customTypes.canConstructCollection(typeRef.getRawType(), 
elementType.getRawType())) {
+      if (ctx.getCustomTypeRegistry()
+          .canConstructCollection(typeRef.getRawType(), 
elementType.getRawType())) {
         return true;
       }
       boolean isSuperOfArrayList = cls.isAssignableFrom(ArrayList.class);
@@ -675,14 +679,8 @@ public class TypeUtils {
       Tuple2<TypeRef<?>, TypeRef<?>> mapKeyValueType = 
getMapKeyValueType(typeRef);
       return isSupported(mapKeyValueType.f0) && 
isSupported(mapKeyValueType.f1);
     } else {
-      if (walkedTypePath.contains(typeRef)) {
-        throw new UnsupportedOperationException(
-            "cyclic type is not supported. walkedTypePath: " + walkedTypePath);
-      } else {
-        LinkedHashSet<TypeRef<?>> newTypePath = new 
LinkedHashSet<>(walkedTypePath);
-        newTypePath.add(typeRef);
-        return isBean(typeRef, newTypePath);
-      }
+      ctx.checkNoCycle(typeRef);
+      return isBean(typeRef, ctx.appendTypePath(typeRef));
     }
   }
 
@@ -699,59 +697,59 @@ public class TypeUtils {
 
   public static LinkedHashSet<Class<?>> listBeansRecursiveInclusive(
       Class<?> beanClass, CustomTypeRegistry customTypes) {
-    return listBeansRecursiveInclusive(beanClass, new LinkedHashSet<>(), 
customTypes);
+    TypeResolutionContext ctx = new TypeResolutionContext(customTypes);
+    if (beanClass.isInterface()) {
+      ctx = ctx.withSynthesizedBeanType(beanClass);
+    }
+    return listBeansRecursiveInclusive(beanClass, ctx);
   }
 
   private static LinkedHashSet<Class<?>> listBeansRecursiveInclusive(
-      Class<?> beanClass,
-      LinkedHashSet<TypeRef<?>> walkedTypePath,
-      CustomTypeRegistry customTypes) {
+      Class<?> beanClass, TypeResolutionContext ctx) {
     LinkedHashSet<Class<?>> beans = new LinkedHashSet<>();
-    Class<?> enclosingType = enclosingType(walkedTypePath);
-    if (customTypes.hasCodec(enclosingType, beanClass)) {
+    Class<?> enclosingType = ctx.getEnclosingType().getRawType();
+    if (ctx.getCustomTypeRegistry().hasCodec(enclosingType, beanClass)) {
       return beans;
     }
-    if (isBean(beanClass, customTypes)) {
+    if (isBean(beanClass, ctx)) {
       beans.add(beanClass);
     }
     LinkedHashSet<TypeRef<?>> typeRefs = new LinkedHashSet<>();
     List<Descriptor> descriptors = Descriptor.getDescriptors(beanClass);
+    TypeResolutionContext newCtx = ctx;
     for (Descriptor descriptor : descriptors) {
       TypeRef<?> typeRef = descriptor.getTypeRef();
+      Class<?> rawType = typeRef.getRawType();
       typeRefs.add(descriptor.getTypeRef());
       typeRefs.addAll(getAllTypeArguments(typeRef));
+      if (beanClass.isInterface() && typeRef.getRawType().isInterface() && 
!isContainer(rawType)) {
+        newCtx = newCtx.withSynthesizedBeanType(typeRef.getRawType());
+      }
     }
 
     for (TypeRef<?> typeToken : typeRefs) {
       Class<?> type = getRawType(typeToken);
-      if (isBean(type, customTypes)) {
-        if (walkedTypePath.contains(typeToken)) {
-          throw new UnsupportedOperationException(
-              "cyclic type is not supported. walkedTypePath: " + 
walkedTypePath);
-        } else {
-          LinkedHashSet<TypeRef<?>> newPath = new 
LinkedHashSet<>(walkedTypePath);
-          newPath.add(typeToken);
-          beans.addAll(listBeansRecursiveInclusive(type, newPath, 
customTypes));
-        }
-      } else if (isCollection(type)) {
+      if (isCollection(type)) {
         TypeRef<?> elementType = getElementType(typeToken);
-        LinkedHashSet<TypeRef<?>> newPath = new 
LinkedHashSet<>(walkedTypePath);
-        newPath.add(elementType);
-        beans.addAll(listBeansRecursiveInclusive(elementType.getClass(), 
newPath, customTypes));
+        while (isContainer(elementType.getRawType())) {
+          elementType = getElementType(elementType);
+        }
+        beans.addAll(
+            listBeansRecursiveInclusive(
+                elementType.getRawType(), newCtx.appendTypePath(elementType)));
       } else if (isMap(type)) {
         Tuple2<TypeRef<?>, TypeRef<?>> mapKeyValueType = 
getMapKeyValueType(typeToken);
-        LinkedHashSet<TypeRef<?>> newPath = new 
LinkedHashSet<>(walkedTypePath);
-        newPath.add(mapKeyValueType.f0);
-        newPath.add(mapKeyValueType.f1);
-        beans.addAll(
-            listBeansRecursiveInclusive(mapKeyValueType.f0.getRawType(), 
newPath, customTypes));
-        beans.addAll(
-            listBeansRecursiveInclusive(mapKeyValueType.f1.getRawType(), 
newPath, customTypes));
+        TypeResolutionContext mapCtx =
+            newCtx.appendTypePath(mapKeyValueType.f0, mapKeyValueType.f1);
+        
beans.addAll(listBeansRecursiveInclusive(mapKeyValueType.f0.getRawType(), 
mapCtx));
+        
beans.addAll(listBeansRecursiveInclusive(mapKeyValueType.f1.getRawType(), 
mapCtx));
       } else if (type.isArray()) {
         Class<?> arrayComponent = getArrayComponent(type);
-        LinkedHashSet<TypeRef<?>> newPath = new 
LinkedHashSet<>(walkedTypePath);
-        newPath.add(TypeRef.of(arrayComponent));
-        beans.addAll(listBeansRecursiveInclusive(arrayComponent, newPath, 
customTypes));
+        beans.addAll(
+            listBeansRecursiveInclusive(arrayComponent, 
newCtx.appendTypePath(arrayComponent)));
+      } else if (isBean(type, newCtx)) {
+        ctx.checkNoCycle(typeToken);
+        beans.addAll(listBeansRecursiveInclusive(type, 
newCtx.appendTypePath(typeToken)));
       }
     }
     return beans;
@@ -809,12 +807,4 @@ public class TypeUtils {
       return pkg + "." + className;
     }
   }
-
-  private static Class<?> enclosingType(LinkedHashSet<TypeRef<?>> newTypePath) 
{
-    Class<?> result = Object.class;
-    for (TypeRef<?> type : newTypePath) {
-      result = type.getRawType();
-    }
-    return result;
-  }
 }
diff --git 
a/java/fury-core/src/test/java/org/apache/fury/CrossLanguageTest.java 
b/java/fury-core/src/test/java/org/apache/fury/CrossLanguageTest.java
index 1ed9474d..bc256672 100644
--- a/java/fury-core/src/test/java/org/apache/fury/CrossLanguageTest.java
+++ b/java/fury-core/src/test/java/org/apache/fury/CrossLanguageTest.java
@@ -467,7 +467,7 @@ public class CrossLanguageTest extends FuryTestBase {
         ObjectSerializer.class.getDeclaredMethod("computeStructHash", 
Fury.class, Collection.class);
     method.setAccessible(true);
     Collection<Descriptor> descriptors =
-        fury.getClassResolver().getAllDescriptorsMap(ComplexObject1.class, 
true).values();
+        fury.getClassResolver().getFieldDescriptors(ComplexObject1.class, 
true);
     descriptors =
         fury.getClassResolver().createDescriptorGrouper(descriptors, 
false).getSortedDescriptors();
     Integer hash = (Integer) method.invoke(serializer, fury, descriptors);
diff --git a/java/fury-format/README.md b/java/fury-format/README.md
index 407479ed..4dee94f7 100644
--- a/java/fury-format/README.md
+++ b/java/fury-format/README.md
@@ -13,16 +13,16 @@ Fury row format is heavily inspired by spark tungsten row 
format, but with chang
 The initial fury java row data structure implementation is modified from spark 
unsafe row/writer.
 
 It is possible to register custom type handling and collection factories for 
the row format -
-see Encoders.registerCustomCodec and Encoders.registerCustomCollectionFactory.
+see Encoders.registerCustomCodec and Encoders.registerCustomCollectionFactory. 
For an interface,
+Fury can synthesize a simple value implementation, such as the UuidType below.
 
 A short example:
 
 ```
-@Data
-public static class UuidType {
-  public UUID f1;
-  public UUID[] f2;
-  public SortedSet<UUID> f3;
+public interface UuidType {
+  UUID f1();
+  UUID[] f2();
+  SortedSet<UUID> f3();
 }
 
 static class UuidEncoder implements CustomCodec.MemoryBufferCodec<UUID> {
diff --git 
a/java/fury-format/src/main/java/org/apache/fury/format/encoder/ArrayDataForEach.java
 
b/java/fury-format/src/main/java/org/apache/fury/format/encoder/ArrayDataForEach.java
index 65fbdff3..aaf711c5 100644
--- 
a/java/fury-format/src/main/java/org/apache/fury/format/encoder/ArrayDataForEach.java
+++ 
b/java/fury-format/src/main/java/org/apache/fury/format/encoder/ArrayDataForEach.java
@@ -31,7 +31,9 @@ import org.apache.fury.codegen.Expression.AbstractExpression;
 import org.apache.fury.format.row.binary.BinaryArray;
 import org.apache.fury.format.row.binary.BinaryUtils;
 import org.apache.fury.format.type.CustomTypeEncoderRegistry;
+import org.apache.fury.format.type.CustomTypeHandler;
 import org.apache.fury.reflect.TypeRef;
+import org.apache.fury.type.TypeResolutionContext;
 import org.apache.fury.type.TypeUtils;
 import org.apache.fury.util.Preconditions;
 import org.apache.fury.util.StringUtils;
@@ -85,8 +87,13 @@ public class ArrayDataForEach extends AbstractExpression {
     } else {
       accessType = TypeRef.of(customEncoder.encodedType());
     }
-    this.accessMethod = BinaryUtils.getElemAccessMethodName(accessType);
-    this.elemType = BinaryUtils.getElemReturnType(accessType);
+    CustomTypeHandler customTypeHandler = 
CustomTypeEncoderRegistry.customTypeHandler();
+    TypeResolutionContext ctx = new TypeResolutionContext(customTypeHandler);
+    if (inputArrayData.type().getRawType().isInterface() && 
elemType.getRawType().isInterface()) {
+      ctx = ctx.withSynthesizedBeanType(elemType.getRawType());
+    }
+    this.accessMethod = BinaryUtils.getElemAccessMethodName(accessType, ctx);
+    this.elemType = BinaryUtils.getElemReturnType(accessType, ctx);
     this.notNullAction = notNullAction;
     this.nullAction = nullAction;
   }
diff --git 
a/java/fury-format/src/main/java/org/apache/fury/format/encoder/ArrayEncoderBuilder.java
 
b/java/fury-format/src/main/java/org/apache/fury/format/encoder/ArrayEncoderBuilder.java
index 4e5479d7..1a5ee5bd 100644
--- 
a/java/fury-format/src/main/java/org/apache/fury/format/encoder/ArrayEncoderBuilder.java
+++ 
b/java/fury-format/src/main/java/org/apache/fury/format/encoder/ArrayEncoderBuilder.java
@@ -48,7 +48,6 @@ public class ArrayEncoderBuilder extends 
BaseBinaryEncoderBuilder {
 
   private static final TypeRef<Field> ARROW_FIELD_TYPE = 
TypeRef.of(Field.class);
   private final TypeRef<?> arrayToken;
-  private final Expression emptyArrayInstance;
 
   public ArrayEncoderBuilder(Class<?> arrayCls, Class<?> beanClass) {
     this(TypeRef.of(arrayCls), TypeRef.of(beanClass));
@@ -79,7 +78,6 @@ public class ArrayEncoderBuilder extends 
BaseBinaryEncoderBuilder {
                 Expression.Literal.ofClass(beanType.getRawType()),
                 Expression.Literal.ofInt(0)),
             TypeRef.of(Array.newInstance(beanType.getRawType(), 
0).getClass())));
-    emptyArrayInstance = new Expression.Reference(ROOT_EMPTY_ARRAY_NAME);
   }
 
   @Override
@@ -148,7 +146,7 @@ public class ArrayEncoderBuilder extends 
BaseBinaryEncoderBuilder {
 
     Expression.Reference fieldExpr = new Expression.Reference(FIELD_NAME, 
ARROW_FIELD_TYPE, false);
     Expression listExpression =
-        serializeForArrayByWriter(emptyArrayInstance, array, arrayWriter, 
arrayToken, fieldExpr);
+        serializeForArrayByWriter(array, arrayWriter, arrayToken, fieldExpr);
 
     expressions.add(listExpression);
 
@@ -183,8 +181,7 @@ public class ArrayEncoderBuilder extends 
BaseBinaryEncoderBuilder {
             arrayData,
             elemType,
             (i, value) ->
-                new Expression.Invoke(
-                    collection, "add", deserializeFor(emptyArrayInstance, 
value, elemType)),
+                new Expression.Invoke(collection, "add", deserializeFor(value, 
elemType, typeCtx)),
             i -> new Expression.Invoke(collection, "add", 
ExpressionUtils.nullValue(elemType)));
     return new Expression.ListExpression(collection, addElemsOp, collection);
   }
diff --git 
a/java/fury-format/src/main/java/org/apache/fury/format/encoder/BaseBinaryEncoderBuilder.java
 
b/java/fury-format/src/main/java/org/apache/fury/format/encoder/BaseBinaryEncoderBuilder.java
index 7125de0a..524ec8eb 100644
--- 
a/java/fury-format/src/main/java/org/apache/fury/format/encoder/BaseBinaryEncoderBuilder.java
+++ 
b/java/fury-format/src/main/java/org/apache/fury/format/encoder/BaseBinaryEncoderBuilder.java
@@ -67,6 +67,7 @@ import org.apache.fury.format.type.DataTypes;
 import org.apache.fury.memory.MemoryBuffer;
 import org.apache.fury.reflect.ReflectionUtils;
 import org.apache.fury.reflect.TypeRef;
+import org.apache.fury.type.TypeResolutionContext;
 import org.apache.fury.type.TypeUtils;
 import org.apache.fury.util.DateTimeUtils;
 import org.apache.fury.util.Preconditions;
@@ -95,6 +96,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
   protected final Map<TypeRef<?>, Reference> rowWriterMap = new HashMap<>();
   protected final CustomTypeHandler customTypeHandler =
       CustomTypeEncoderRegistry.customTypeHandler();
+  protected final TypeResolutionContext typeCtx;
 
   public BaseBinaryEncoderBuilder(CodegenContext context, Class<?> beanClass) {
     this(context, TypeRef.of(beanClass));
@@ -107,6 +109,12 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
     ctx.addImport(BinaryRow.class.getPackage().getName() + ".*");
     ctx.addImport(BinaryWriter.class.getPackage().getName() + ".*");
     ctx.addImport(Schema.class.getPackage().getName() + ".*");
+    TypeResolutionContext typeCtx = new 
TypeResolutionContext(customTypeHandler);
+    typeCtx.appendTypePath(beanClass);
+    if (beanClass.isInterface()) {
+      typeCtx = typeCtx.withSynthesizedBeanType(beanClass);
+    }
+    this.typeCtx = typeCtx;
   }
 
   public String codecClassName(Class<?> beanClass) {
@@ -140,7 +148,6 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
    * writer</code>
    */
   protected Expression serializeFor(
-      Expression bean,
       Expression ordinal,
       Expression inputObject,
       Expression writer,
@@ -157,10 +164,10 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
               "rewrittenValue",
               rewrittenType,
               true,
-              bean,
+              new Expression.Null(beanType, true),
               inputObject);
       Expression doSerialize =
-          serializeFor(bean, ordinal, newInputObject, writer, rewrittenType, 
arrowField);
+          serializeFor(ordinal, newInputObject, writer, rewrittenType, 
arrowField);
       return new If(
           ExpressionUtils.eqNull(inputObject),
           new Invoke(writer, "setNullAt", ordinal),
@@ -224,7 +231,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
       // but don't setOffsetAndSize for array.
       Invoke offset =
           new Invoke(writer, "writerIndex", "writerIndex", 
TypeUtils.PRIMITIVE_INT_TYPE);
-      Expression serializeArray = serializeForArray(bean, inputObject, writer, 
typeRef, arrowField);
+      Expression serializeArray = serializeForArray(inputObject, writer, 
typeRef, arrowField);
       Arithmetic size =
           ExpressionUtils.subtract(
               new Invoke(writer, "writerIndex", "writerIndex", 
TypeUtils.PRIMITIVE_INT_TYPE),
@@ -237,8 +244,8 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
           new Invoke(writer, "setNullAt", ordinal),
           expression);
     } else if (TypeUtils.MAP_TYPE.isSupertypeOf(typeRef)) {
-      return serializeForMap(bean, ordinal, writer, inputObject, typeRef, 
arrowField);
-    } else if (TypeUtils.isBean(rawType)) {
+      return serializeForMap(ordinal, writer, inputObject, typeRef, 
arrowField);
+    } else if (TypeUtils.isBean(rawType, createElementTypeContext(typeRef))) {
       return serializeForBean(ordinal, writer, inputObject, typeRef, 
arrowField);
     } else if (rawType == BinaryArray.class) {
       Invoke writeExp =
@@ -270,21 +277,13 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
   }
 
   protected Expression serializeForArray(
-      Expression bean,
-      Expression inputObject,
-      Expression writer,
-      TypeRef<?> typeRef,
-      Expression arrowField) {
+      Expression inputObject, Expression writer, TypeRef<?> typeRef, 
Expression arrowField) {
     Reference arrayWriter = getOrCreateArrayWriter(typeRef, arrowField, 
writer);
-    return serializeForArrayByWriter(bean, inputObject, arrayWriter, typeRef, 
arrowField);
+    return serializeForArrayByWriter(inputObject, arrayWriter, typeRef, 
arrowField);
   }
 
   protected Expression serializeForArrayByWriter(
-      Expression bean,
-      Expression inputObject,
-      Expression arrayWriter,
-      TypeRef<?> typeRef,
-      Expression arrowField) {
+      Expression inputObject, Expression arrayWriter, TypeRef<?> typeRef, 
Expression arrowField) {
     StaticInvoke arrayElementField =
         new StaticInvoke(
             DataTypes.class, "arrayElementField", "elemField", 
ARROW_FIELD_TYPE, false, arrowField);
@@ -301,7 +300,6 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
                 inputObject,
                 (i, value) ->
                     serializeFor(
-                        bean,
                         i,
                         value,
                         arrayWriter,
@@ -318,12 +316,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
               listFromIterable,
               (i, value) ->
                   serializeFor(
-                      bean,
-                      i,
-                      value,
-                      arrayWriter,
-                      TypeUtils.getElementType(typeRef),
-                      arrayElementField));
+                      i, value, arrayWriter, 
TypeUtils.getElementType(typeRef), arrayElementField));
       return new ListExpression(reset, forEach, arrayWriter);
     } else { // collection
       Invoke size = new Invoke(inputObject, "size", 
TypeUtils.PRIMITIVE_INT_TYPE);
@@ -333,12 +326,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
               inputObject,
               (i, value) ->
                   serializeFor(
-                      bean,
-                      i,
-                      value,
-                      arrayWriter,
-                      TypeUtils.getElementType(typeRef),
-                      arrayElementField));
+                      i, value, arrayWriter, 
TypeUtils.getElementType(typeRef), arrayElementField));
       return new ListExpression(reset, forEach, arrayWriter);
     }
   }
@@ -362,7 +350,6 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
    * row/array using given <code>writer</code>.
    */
   protected Expression serializeForMap(
-      Expression bean,
       Expression ordinal,
       Expression writer,
       Expression inputObject,
@@ -398,8 +385,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
     expressions.add(offset, preserve);
 
     Invoke keySet = new Invoke(inputObject, "keySet", keySetType);
-    Expression keySerializationExpr =
-        serializeForArray(bean, keySet, writer, keySetType, keyArrayField);
+    Expression keySerializationExpr = serializeForArray(keySet, writer, 
keySetType, keyArrayField);
     expressions.add(keySet, keySerializationExpr);
 
     expressions.add(
@@ -411,7 +397,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
 
     Invoke values = new Invoke(inputObject, "values", valuesType);
     Expression valueSerializationExpr =
-        serializeForArray(bean, values, writer, valuesType, valueArrayField);
+        serializeForArray(values, writer, valuesType, valueArrayField);
     expressions.add(values, valueSerializationExpr);
 
     Arithmetic size =
@@ -524,7 +510,8 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
    * Returns an expression that deserialize <code>value</code> as a java 
object of type <code>
    * typeToken</code>.
    */
-  protected Expression deserializeFor(Expression bean, Expression value, 
TypeRef<?> typeRef) {
+  protected Expression deserializeFor(
+      Expression value, TypeRef<?> typeRef, TypeResolutionContext ctx) {
     Class<?> rawType = getRawType(typeRef);
     CustomCodec<?, ?> customHandler = 
customTypeHandler.findCodec(beanType.getRawType(), rawType);
     if (customHandler != null) {
@@ -543,7 +530,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
               "decodedValue",
               typeRef,
               true,
-              bean,
+              new Expression.Null(beanType, true),
               new Expression.Null(typeRef, true),
               inputValue);
       if (rawRewrittenType == MemoryBuffer.class) {
@@ -553,7 +540,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
       } else if (rawRewrittenType == byte[].class) {
         return newValue;
       }
-      return deserializeFor(bean, newValue, rewrittenType);
+      return deserializeFor(newValue, rewrittenType, ctx);
     } else if (TypeUtils.isPrimitive(rawType) || TypeUtils.isBoxed(rawType)) {
       return value;
     } else if (rawType == BigDecimal.class) {
@@ -576,12 +563,12 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
     } else if (rawType.isEnum()) {
       return ExpressionUtils.valueOf(typeRef, value);
     } else if (rawType.isArray()) {
-      return deserializeForArray(bean, value, typeRef);
+      return deserializeForArray(value, typeRef);
     } else if (TypeUtils.ITERABLE_TYPE.isSupertypeOf(typeRef)) {
-      return deserializeForCollection(bean, value, typeRef);
+      return deserializeForCollection(value, typeRef);
     } else if (TypeUtils.MAP_TYPE.isSupertypeOf(typeRef)) {
-      return deserializeForMap(bean, value, typeRef);
-    } else if (TypeUtils.isBean(rawType)) {
+      return deserializeForMap(value, typeRef);
+    } else if (TypeUtils.isBean(rawType, ctx)) {
       return deserializeForBean(value, typeRef);
     } else {
       return deserializeForObject(value, typeRef);
@@ -602,7 +589,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
   }
 
   /** Returns an expression that deserialize <code>mapData</code> as a java 
map. */
-  protected Expression deserializeForMap(Expression bean, Expression mapData, 
TypeRef<?> typeRef) {
+  protected Expression deserializeForMap(Expression mapData, TypeRef<?> 
typeRef) {
     Expression javaMap = newMap(typeRef);
     @SuppressWarnings("unchecked")
     TypeRef<?> supertype = ((TypeRef<? extends Map<?, ?>>) 
typeRef).getSupertype(Map.class);
@@ -614,14 +601,14 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
     Expression keyJavaArray;
     Expression valueJavaArray;
     if (TypeUtils.ITERABLE_TYPE.isSupertypeOf(keysType)) {
-      keyJavaArray = deserializeForCollection(bean, keyArray, keysType);
+      keyJavaArray = deserializeForCollection(keyArray, keysType);
     } else {
-      keyJavaArray = deserializeForArray(bean, keyArray, keysType);
+      keyJavaArray = deserializeForArray(keyArray, keysType);
     }
     if (TypeUtils.ITERABLE_TYPE.isSupertypeOf(valuesType)) {
-      valueJavaArray = deserializeForCollection(bean, valueArray, valuesType);
+      valueJavaArray = deserializeForCollection(valueArray, valuesType);
     } else {
-      valueJavaArray = deserializeForArray(bean, valueArray, valuesType);
+      valueJavaArray = deserializeForArray(valueArray, valuesType);
     }
 
     ZipForEach put =
@@ -634,8 +621,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
   }
 
   /** Returns an expression that deserialize <code>arrayData</code> as a java 
collection. */
-  protected Expression deserializeForCollection(
-      Expression bean, Expression arrayData, TypeRef<?> typeRef) {
+  protected Expression deserializeForCollection(Expression arrayData, 
TypeRef<?> typeRef) {
     Expression collection = newCollection(arrayData, typeRef);
     try {
       TypeRef<?> elemType = TypeUtils.getElementType(typeRef);
@@ -643,7 +629,11 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
           new ArrayDataForEach(
               arrayData,
               elemType,
-              (i, value) -> new Invoke(collection, "add", deserializeFor(bean, 
value, elemType)),
+              (i, value) ->
+                  new Invoke(
+                      collection,
+                      "add",
+                      deserializeFor(value, elemType, 
createElementTypeContext(elemType))),
               i -> new Invoke(collection, "add", 
ExpressionUtils.nullValue(elemType)));
       return new ListExpression(collection, addElemsOp, collection);
     } catch (Exception e) {
@@ -712,7 +702,6 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
    * <code>rootJavaArray</code>.
    */
   protected Expression deserializeForMultiDimensionArray(
-      Expression bean,
       Expression arrayData,
       Expression rootJavaArray,
       int numDimensions,
@@ -729,8 +718,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
             Expression[] newIndexes = Arrays.copyOf(indexes, indexes.length + 
1);
             newIndexes[indexes.length] = i;
             Expression elemArr =
-                deserializeForArray(
-                    bean, value, 
Objects.requireNonNull(typeRef.getComponentType()));
+                deserializeForArray(value, 
Objects.requireNonNull(typeRef.getComponentType()));
             return new AssignArrayElem(rootJavaArray, elemArr, newIndexes);
           });
     } else {
@@ -741,7 +729,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
             Expression[] newIndexes = Arrays.copyOf(indexes, indexes.length + 
1);
             newIndexes[indexes.length] = i;
             return deserializeForMultiDimensionArray(
-                bean, value, rootJavaArray, numDimensions - 1, elemType, 
newIndexes);
+                value, rootJavaArray, numDimensions - 1, elemType, newIndexes);
           });
     }
   }
@@ -750,8 +738,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
    * Return an expression that deserialize <code>arrayData</code>. If array is 
multi-array, forward
    * to {@link BaseBinaryEncoderBuilder#deserializeForMultiDimensionArray}
    */
-  protected Expression deserializeForArray(
-      Expression bean, Expression arrayData, TypeRef<?> typeRef) {
+  protected Expression deserializeForArray(Expression arrayData, TypeRef<?> 
typeRef) {
     int numDimensions = TypeUtils.getArrayDimensions(typeRef);
     if (numDimensions > 1) {
       // If some dimension's elements is all null, we take outer-most array as 
null,
@@ -770,7 +757,7 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
       Expression rootJavaMultiDimArray = new NewArray(innerElemClass, 
numDimensions, dimensions);
       Expression op =
           deserializeForMultiDimensionArray(
-              bean, arrayData, rootJavaMultiDimArray, numDimensions, typeRef, 
new Expression[0]);
+              arrayData, rootJavaMultiDimArray, numDimensions, typeRef, new 
Expression[0]);
       // although the value maybe null, we don't use this info, so we set 
nullability to false.
       return new If(
           ExpressionUtils.notNull(dimensions),
@@ -802,7 +789,8 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
                 arrayData,
                 elemType,
                 (i, value) -> {
-                  Expression elemValue = deserializeFor(bean, value, elemType);
+                  Expression elemValue =
+                      deserializeFor(value, elemType, 
createElementTypeContext(elemType));
                   return new AssignArrayElem(javaArray, elemValue, i);
                 });
         // add javaArray at last as expression value
@@ -818,4 +806,14 @@ public abstract class BaseBinaryEncoderBuilder extends 
CodecBuilder {
   protected Expression deserializeForObject(Expression value, TypeRef<?> 
typeRef) {
     return new Invoke(furyRef, "deserialize", typeRef, value);
   }
+
+  protected TypeResolutionContext createElementTypeContext(TypeRef<?> 
elemType) {
+    TypeResolutionContext newTypeCtx;
+    if (elemType.isInterface() && beanClass.isInterface()) {
+      newTypeCtx = typeCtx.withSynthesizedBeanType(elemType.getRawType());
+    } else {
+      newTypeCtx = typeCtx;
+    }
+    return newTypeCtx;
+  }
 }
diff --git 
a/java/fury-format/src/main/java/org/apache/fury/format/encoder/MapEncoderBuilder.java
 
b/java/fury-format/src/main/java/org/apache/fury/format/encoder/MapEncoderBuilder.java
index cf0d0fc6..a005340f 100644
--- 
a/java/fury-format/src/main/java/org/apache/fury/format/encoder/MapEncoderBuilder.java
+++ 
b/java/fury-format/src/main/java/org/apache/fury/format/encoder/MapEncoderBuilder.java
@@ -53,7 +53,6 @@ public class MapEncoderBuilder extends 
BaseBinaryEncoderBuilder {
   private static final String ROOT_VALUE_WRITER_NAME = "valueArrayWriter";
 
   private static final TypeRef<Field> ARROW_FIELD_TYPE = 
TypeRef.of(Field.class);
-  private static final Expression MAP_TYPE_EXPR = 
Expression.Literal.ofClass(Map.class);
   private final TypeRef<?> mapToken;
 
   public MapEncoderBuilder(Class<?> mapCls, Class<?> keyClass) {
@@ -181,7 +180,7 @@ public class MapEncoderBuilder extends 
BaseBinaryEncoderBuilder {
     expressions.add(
         new Expression.Invoke(keyArrayWriter, "writeDirectly", 
Expression.Literal.ofInt(-1)));
     Expression keySerializationExpr =
-        serializeForArrayByWriter(MAP_TYPE_EXPR, keySet, keyArrayWriter, 
keySetType, keyFieldExpr);
+        serializeForArrayByWriter(keySet, keyArrayWriter, keySetType, 
keyFieldExpr);
     Expression.Invoke keyArray =
         new Expression.Invoke(keyArrayWriter, "toArray", 
TypeRef.of(BinaryArray.class));
     expressions.add(map);
@@ -196,7 +195,7 @@ public class MapEncoderBuilder extends 
BaseBinaryEncoderBuilder {
 
     Expression.Invoke values = new Expression.Invoke(map, "values", 
valuesType);
     Expression valueSerializationExpr =
-        serializeForArrayByWriter(MAP_TYPE_EXPR, values, valArrayWriter, 
valuesType, valFieldExpr);
+        serializeForArrayByWriter(values, valArrayWriter, valuesType, 
valFieldExpr);
     Expression.Invoke valArray =
         new Expression.Invoke(valArrayWriter, "toArray", 
TypeRef.of(BinaryArray.class));
 
@@ -213,6 +212,7 @@ public class MapEncoderBuilder extends 
BaseBinaryEncoderBuilder {
    * Returns an expression that deserialize <code>row</code> as a java bean of 
type {@link
    * MapEncoderBuilder#mapToken}.
    */
+  @Override
   public Expression buildDecodeExpression() {
     Expression.ListExpression expressions = new Expression.ListExpression();
     Expression map = newMap(mapToken);
@@ -238,14 +238,14 @@ public class MapEncoderBuilder extends 
BaseBinaryEncoderBuilder {
     Expression keyJavaArray;
     Expression valueJavaArray;
     if (TypeUtils.ITERABLE_TYPE.isSupertypeOf(keysType)) {
-      keyJavaArray = deserializeForCollection(MAP_TYPE_EXPR, keyArrayRef, 
keysType);
+      keyJavaArray = deserializeForCollection(keyArrayRef, keysType);
     } else {
-      keyJavaArray = deserializeForArray(MAP_TYPE_EXPR, keyArrayRef, keysType);
+      keyJavaArray = deserializeForArray(keyArrayRef, keysType);
     }
     if (TypeUtils.ITERABLE_TYPE.isSupertypeOf(valuesType)) {
-      valueJavaArray = deserializeForCollection(MAP_TYPE_EXPR, valArrayRef, 
valuesType);
+      valueJavaArray = deserializeForCollection(valArrayRef, valuesType);
     } else {
-      valueJavaArray = deserializeForArray(MAP_TYPE_EXPR, valArrayRef, 
valuesType);
+      valueJavaArray = deserializeForArray(valArrayRef, valuesType);
     }
 
     Expression.ZipForEach put =
diff --git 
a/java/fury-format/src/main/java/org/apache/fury/format/encoder/RowEncoderBuilder.java
 
b/java/fury-format/src/main/java/org/apache/fury/format/encoder/RowEncoderBuilder.java
index 6ea20553..4b56feac 100644
--- 
a/java/fury-format/src/main/java/org/apache/fury/format/encoder/RowEncoderBuilder.java
+++ 
b/java/fury-format/src/main/java/org/apache/fury/format/encoder/RowEncoderBuilder.java
@@ -48,6 +48,7 @@ import org.apache.fury.logging.Logger;
 import org.apache.fury.logging.LoggerFactory;
 import org.apache.fury.reflect.TypeRef;
 import org.apache.fury.type.Descriptor;
+import org.apache.fury.type.TypeResolutionContext;
 import org.apache.fury.type.TypeUtils;
 import org.apache.fury.util.GraalvmSupport;
 import org.apache.fury.util.Preconditions;
@@ -61,10 +62,13 @@ public class RowEncoderBuilder extends 
BaseBinaryEncoderBuilder {
   static final String ROOT_ROW_NAME = "row";
   static final String ROOT_ROW_WRITER_NAME = "rowWriter";
 
+  private final String className;
   private final SortedMap<String, Descriptor> descriptorsMap;
   private final Schema schema;
   protected static final String BEAN_CLASS_NAME = "beanClass";
   protected Reference beanClassRef = new Reference(BEAN_CLASS_NAME, 
CLASS_TYPE);
+  private final CodegenContext generatedBeanImpl;
+  private final String generatedBeanImplName;
 
   public RowEncoderBuilder(Class<?> beanClass) {
     this(TypeRef.of(beanClass));
@@ -72,7 +76,9 @@ public class RowEncoderBuilder extends 
BaseBinaryEncoderBuilder {
 
   public RowEncoderBuilder(TypeRef<?> beanType) {
     super(new CodegenContext(), beanType);
-    Preconditions.checkArgument(TypeUtils.isBean(beanType.getType(), 
customTypeHandler));
+    Preconditions.checkArgument(
+        beanClass.isInterface() || TypeUtils.isBean(beanType.getType(), 
customTypeHandler));
+    className = codecClassName(beanClass);
     this.schema = TypeInference.inferSchema(getRawType(beanType));
     this.descriptorsMap = Descriptor.getDescriptorsMap(beanClass);
     ctx.reserveName(ROOT_ROW_WRITER_NAME);
@@ -86,12 +92,19 @@ public class RowEncoderBuilder extends 
BaseBinaryEncoderBuilder {
       // non-public class is not accessible in other class.
       clsExpr =
           new Expression.StaticInvoke(
-              Class.class, "forName", CLASS_TYPE, false, 
Literal.ofClass(beanClass));
+              Class.class, "forName", CLASS_TYPE, false, 
Literal.ofString(ctx.type(beanClass)));
     }
     ctx.addField(Class.class, "beanClass", clsExpr);
     ctx.addImports(Field.class, Schema.class);
     ctx.addImports(Row.class, ArrayData.class, MapData.class);
     ctx.addImports(BinaryRow.class, BinaryArray.class, BinaryMap.class);
+    if (beanClass.isInterface()) {
+      generatedBeanImplName = beanClass.getSimpleName() + "GeneratedImpl";
+      generatedBeanImpl = buildImplClass();
+    } else {
+      generatedBeanImplName = null;
+      generatedBeanImpl = null;
+    }
   }
 
   @Override
@@ -102,7 +115,6 @@ public class RowEncoderBuilder extends 
BaseBinaryEncoderBuilder {
   @Override
   public String genCode() {
     ctx.setPackage(CodeGenerator.getPackage(beanClass));
-    String className = codecClassName(beanClass);
     ctx.setClassName(className);
     // don't addImport(beanClass), because user class may name collide.
     // janino don't support generics, so GeneratedCodec has no generics
@@ -141,6 +153,14 @@ public class RowEncoderBuilder extends 
BaseBinaryEncoderBuilder {
 
     long startTime = System.nanoTime();
     String code = ctx.genCode();
+    // It would be nice if Expression let us write inner classes
+    if (generatedBeanImpl != null) {
+      int insertPoint = code.lastIndexOf('}');
+      code =
+          code.substring(0, insertPoint)
+              + generatedBeanImpl.genCode()
+              + code.substring(insertPoint);
+    }
     long durationMs = (System.nanoTime() - startTime) / 1000;
     LOG.info("Generate codec for class {} take {} us", beanClass, durationMs);
     return code;
@@ -169,7 +189,7 @@ public class RowEncoderBuilder extends 
BaseBinaryEncoderBuilder {
       Expression.StaticInvoke field =
           new Expression.StaticInvoke(
               DataTypes.class, "fieldOfSchema", ARROW_FIELD_TYPE, false, 
schemaExpr, ordinal);
-      Expression fieldExpr = serializeFor(bean, ordinal, fieldValue, writer, 
fieldType, field);
+      Expression fieldExpr = serializeFor(ordinal, fieldValue, writer, 
fieldType, field);
       expressions.add(fieldExpr);
     }
     expressions.add(
@@ -185,6 +205,13 @@ public class RowEncoderBuilder extends 
BaseBinaryEncoderBuilder {
   @Override
   public Expression buildDecodeExpression() {
     Reference row = new Reference(ROOT_ROW_NAME, binaryRowTypeToken, false);
+
+    addDecoderMethods();
+
+    if (generatedBeanImpl != null) {
+      return new Expression.Return(
+          new Expression.Reference("new " + generatedBeanImplName + "(row)"));
+    }
     Expression bean = newBean();
 
     int numFields = schema.getFields().size();
@@ -197,16 +224,42 @@ public class RowEncoderBuilder extends 
BaseBinaryEncoderBuilder {
       TypeRef<?> fieldType = d.getTypeRef();
       Expression.Invoke isNullAt =
           new Expression.Invoke(row, "isNullAt", 
TypeUtils.PRIMITIVE_BOOLEAN_TYPE, ordinal);
-      CustomCodec<?, ?> customEncoder =
-          customTypeHandler.findCodec(beanClass, fieldType.getRawType());
+      Expression value =
+          new Expression.Variable(
+              "decoded" + i, new Expression.Reference("decode" + i + "(row)", 
fieldType));
+      Expression setActionExpr = setFieldValue(bean, d, value);
+      Expression action = new Expression.If(ExpressionUtils.not(isNullAt), 
setActionExpr);
+      expressions.add(action);
+    }
+
+    expressions.add(new Expression.Return(bean));
+    return expressions;
+  }
+
+  private void addDecoderMethods() {
+    Reference row = new Reference(ROOT_ROW_NAME, binaryRowTypeToken, false);
+    int numFields = schema.getFields().size();
+    for (int i = 0; i < numFields; i++) {
+      Literal ordinal = Literal.ofInt(i);
+      Descriptor d = 
getDescriptorByFieldName(schema.getFields().get(i).getName());
+      TypeRef<?> fieldType = d.getTypeRef();
+      Class<?> rawFieldType = fieldType.getRawType();
+      TypeResolutionContext fieldCtx;
+      if (beanClass.isInterface() && rawFieldType.isInterface()) {
+        fieldCtx = typeCtx.withSynthesizedBeanType(rawFieldType);
+      } else {
+        fieldCtx = typeCtx;
+      }
+      CustomCodec<?, ?> customEncoder = customTypeHandler.findCodec(beanClass, 
rawFieldType);
       TypeRef<?> columnAccessType;
       if (customEncoder == null) {
         columnAccessType = fieldType;
       } else {
         columnAccessType = TypeRef.of(customEncoder.encodedType());
       }
-      String columnAccessMethodName = 
BinaryUtils.getElemAccessMethodName(columnAccessType);
-      TypeRef<?> colType = BinaryUtils.getElemReturnType(columnAccessType);
+      String columnAccessMethodName =
+          BinaryUtils.getElemAccessMethodName(columnAccessType, fieldCtx);
+      TypeRef<?> colType = BinaryUtils.getElemReturnType(columnAccessType, 
fieldCtx);
       Expression.Invoke columnValue =
           new Expression.Invoke(
               row,
@@ -215,14 +268,64 @@ public class RowEncoderBuilder extends 
BaseBinaryEncoderBuilder {
               colType,
               false,
               ordinal);
-      Expression value = deserializeFor(bean, columnValue, fieldType);
-      Expression setActionExpr = setFieldValue(bean, d, value);
-      Expression action = new Expression.If(ExpressionUtils.not(isNullAt), 
setActionExpr);
-      expressions.add(action);
+      final Expression value =
+          new Expression.Return(deserializeFor(columnValue, fieldType, 
fieldCtx));
+      ctx.addMethod(
+          "decode" + i,
+          value.doGenCode(ctx).code(),
+          fieldType.getRawType(),
+          BinaryRow.class,
+          ROOT_ROW_NAME);
     }
+  }
 
-    expressions.add(new Expression.Return(bean));
-    return expressions;
+  private CodegenContext buildImplClass() {
+    Reference row = new Reference(ROOT_ROW_NAME, binaryRowTypeToken, false);
+    CodegenContext implClass = new CodegenContext();
+    implClass.setClassModifiers("final");
+    implClass.setClassName(generatedBeanImplName);
+    implClass.implementsInterfaces(implClass.type(beanClass));
+    implClass.addField(true, implClass.type(BinaryRow.class), "row", null);
+    implClass.addConstructor("this.row = row;", BinaryRow.class, "row");
+
+    int numFields = schema.getFields().size();
+    for (int i = 0; i < numFields; i++) {
+      Literal ordinal = Literal.ofInt(i);
+      Descriptor d = 
getDescriptorByFieldName(schema.getFields().get(i).getName());
+      TypeRef<?> fieldType = d.getTypeRef();
+
+      Expression.Reference decodeValue =
+          new Expression.Reference("decode" + i + "(row)", fieldType);
+      Expression getterImpl;
+      if (fieldType.isPrimitive()) {
+        getterImpl = new Expression.Return(decodeValue);
+      } else {
+        String fieldName = "f" + i + "_" + d.getName();
+        implClass.addField(fieldType.getRawType(), fieldName);
+
+        Expression fieldRef = new Expression.Reference(fieldName, fieldType, 
true);
+        Expression storeValue =
+            new Expression.SetField(new Expression.Reference("this"), 
fieldName, decodeValue);
+        Expression loadIfFieldIsNull =
+            new Expression.If(new Expression.IsNull(fieldRef), storeValue);
+        Expression assigner;
+
+        if (d.isNullable()) {
+          Expression isNotNullAt =
+              new Expression.Not(
+                  new Expression.Invoke(
+                      row, "isNullAt", TypeUtils.PRIMITIVE_BOOLEAN_TYPE, 
ordinal));
+          assigner = new Expression.If(isNotNullAt, loadIfFieldIsNull);
+        } else {
+          assigner = loadIfFieldIsNull;
+        }
+        getterImpl = new Expression.ListExpression(assigner, new 
Expression.Return(fieldRef));
+      }
+      implClass.addMethod(
+          d.getName(), getterImpl.genCode(implClass).code(), 
fieldType.getRawType());
+    }
+
+    return implClass;
   }
 
   private Descriptor getDescriptorByFieldName(String fieldName) {
diff --git 
a/java/fury-format/src/main/java/org/apache/fury/format/row/binary/BinaryUtils.java
 
b/java/fury-format/src/main/java/org/apache/fury/format/row/binary/BinaryUtils.java
index f16ada2a..d488b0b1 100644
--- 
a/java/fury-format/src/main/java/org/apache/fury/format/row/binary/BinaryUtils.java
+++ 
b/java/fury-format/src/main/java/org/apache/fury/format/row/binary/BinaryUtils.java
@@ -21,12 +21,13 @@ package org.apache.fury.format.row.binary;
 
 import org.apache.fury.memory.MemoryBuffer;
 import org.apache.fury.reflect.TypeRef;
+import org.apache.fury.type.TypeResolutionContext;
 import org.apache.fury.type.TypeUtils;
 
 /** Util class for building generated binary encoder. */
 @SuppressWarnings("UnstableApiUsage")
 public class BinaryUtils {
-  public static String getElemAccessMethodName(TypeRef<?> type) {
+  public static String getElemAccessMethodName(TypeRef<?> type, 
TypeResolutionContext ctx) {
     if (TypeUtils.PRIMITIVE_BYTE_TYPE.equals(type) || 
TypeUtils.BYTE_TYPE.equals(type)) {
       return "getByte";
     } else if (TypeUtils.PRIMITIVE_BOOLEAN_TYPE.equals(type)
@@ -57,7 +58,7 @@ public class BinaryUtils {
       return "getArray";
     } else if (TypeUtils.MAP_TYPE.isSupertypeOf(type)) {
       return "getMap";
-    } else if (TypeUtils.isBean(type)) {
+    } else if (TypeUtils.isBean(type, ctx)) {
       return "getStruct";
     } else {
       // take unknown type as OBJECT_TYPE, return as sliced MemoryBuffer
@@ -66,7 +67,7 @@ public class BinaryUtils {
     }
   }
 
-  public static TypeRef<?> getElemReturnType(TypeRef<?> type) {
+  public static TypeRef<?> getElemReturnType(TypeRef<?> type, 
TypeResolutionContext ctx) {
     if (TypeUtils.PRIMITIVE_BYTE_TYPE.equals(type) || 
TypeUtils.BYTE_TYPE.equals(type)) {
       return TypeUtils.PRIMITIVE_BYTE_TYPE;
     } else if (TypeUtils.PRIMITIVE_BOOLEAN_TYPE.equals(type)
@@ -95,7 +96,7 @@ public class BinaryUtils {
       return TypeRef.of(BinaryArray.class);
     } else if (TypeUtils.MAP_TYPE.isSupertypeOf(type)) {
       return TypeRef.of(BinaryMap.class);
-    } else if (TypeUtils.isBean(type)) {
+    } else if (TypeUtils.isBean(type, ctx)) {
       return TypeRef.of(BinaryRow.class);
     } else {
       // take unknown type as OBJECT_TYPE, return as sliced MemoryBuffer
diff --git 
a/java/fury-format/src/main/java/org/apache/fury/format/type/TypeInference.java 
b/java/fury-format/src/main/java/org/apache/fury/format/type/TypeInference.java
index 7d3f963a..935a0cd9 100644
--- 
a/java/fury-format/src/main/java/org/apache/fury/format/type/TypeInference.java
+++ 
b/java/fury-format/src/main/java/org/apache/fury/format/type/TypeInference.java
@@ -24,7 +24,6 @@ import static org.apache.fury.type.TypeUtils.getRawType;
 
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
@@ -42,6 +41,7 @@ import org.apache.fury.format.encoder.CustomCodec;
 import org.apache.fury.format.encoder.CustomCollectionFactory;
 import org.apache.fury.reflect.TypeRef;
 import org.apache.fury.type.Descriptor;
+import org.apache.fury.type.TypeResolutionContext;
 import org.apache.fury.type.TypeUtils;
 import org.apache.fury.util.DecimalUtils;
 import org.apache.fury.util.Preconditions;
@@ -120,13 +120,18 @@ public class TypeInference {
   }
 
   private static Field inferField(TypeRef<?> arrayTypeRef, TypeRef<?> typeRef) 
{
-    LinkedHashSet<Class<?>> seenTypeSet = new LinkedHashSet<>();
+    TypeResolutionContext ctx =
+        new 
TypeResolutionContext(CustomTypeEncoderRegistry.customTypeHandler());
+    Class<?> clz = getRawType(typeRef);
+    if (clz.isInterface()) {
+      ctx = ctx.withSynthesizedBeanType(clz);
+    }
     String name = "";
     if (arrayTypeRef != null) {
-      Field f = inferField(DataTypes.ARRAY_ITEM_NAME, typeRef, seenTypeSet);
+      Field f = inferField(DataTypes.ARRAY_ITEM_NAME, typeRef, ctx);
       return DataTypes.arrayField(name, f);
     } else {
-      return inferField("", typeRef, seenTypeSet);
+      return inferField("", typeRef, ctx);
     }
   }
 
@@ -136,19 +141,11 @@ public class TypeInference {
    *
    * @return DataType of a typeToken
    */
-  private static Field inferField(
-      String name, TypeRef<?> typeRef, LinkedHashSet<Class<?>> seenTypeSet) {
-    return inferField(name, typeRef, seenTypeSet, 
CustomTypeEncoderRegistry.customTypeHandler());
-  }
-
-  private static Field inferField(
-      String name,
-      TypeRef<?> typeRef,
-      LinkedHashSet<Class<?>> seenTypeSet,
-      CustomTypeHandler customTypes) {
+  private static Field inferField(String name, TypeRef<?> typeRef, 
TypeResolutionContext ctx) {
     Class<?> rawType = getRawType(typeRef);
-    Class<?> enclosingType = enclosingType(seenTypeSet);
-    CustomCodec<?, ?> customEncoder = customTypes.findCodec(enclosingType, 
rawType);
+    Class<?> enclosingType = ctx.getEnclosingType().getRawType();
+    CustomCodec<?, ?> customEncoder =
+        ((CustomTypeHandler) 
ctx.getCustomTypeRegistry()).findCodec(enclosingType, rawType);
     if (customEncoder != null) {
       return customEncoder.getField(name);
     } else if (rawType == boolean.class) {
@@ -207,53 +204,44 @@ public class TypeInference {
     } else if (rawType.isArray()) { // array
       Field f =
           inferField(
-              DataTypes.ARRAY_ITEM_NAME,
-              Objects.requireNonNull(typeRef.getComponentType()),
-              seenTypeSet,
-              customTypes);
+              DataTypes.ARRAY_ITEM_NAME, 
Objects.requireNonNull(typeRef.getComponentType()), ctx);
       return DataTypes.arrayField(name, f);
     } else if (TypeUtils.ITERABLE_TYPE.isSupertypeOf(typeRef)) { // iterable
       // when type is both iterable and bean, we take it as iterable in 
row-format
-      Field f =
-          inferField(
-              DataTypes.ARRAY_ITEM_NAME,
-              TypeUtils.getElementType(typeRef),
-              seenTypeSet,
-              customTypes);
+      Field f = inferField(DataTypes.ARRAY_ITEM_NAME, 
TypeUtils.getElementType(typeRef), ctx);
       return DataTypes.arrayField(name, f);
     } else if (TypeUtils.MAP_TYPE.isSupertypeOf(typeRef)) {
       Tuple2<TypeRef<?>, TypeRef<?>> kvType = 
TypeUtils.getMapKeyValueType(typeRef);
-      Field keyField = inferField(MapVector.KEY_NAME, kvType.f0, seenTypeSet, 
customTypes);
+      Field keyField = inferField(MapVector.KEY_NAME, kvType.f0, ctx);
       // Map's keys must be non-nullable
       FieldType keyFieldType =
           new FieldType(
               false, keyField.getType(), keyField.getDictionary(), 
keyField.getMetadata());
       keyField = DataTypes.field(keyField.getName(), keyFieldType, 
keyField.getChildren());
-      Field valueField = inferField(MapVector.VALUE_NAME, kvType.f1, 
seenTypeSet, customTypes);
+      Field valueField = inferField(MapVector.VALUE_NAME, kvType.f1, ctx);
       return DataTypes.mapField(name, keyField, valueField);
-    } else if (TypeUtils.isBean(rawType, customTypes)) { // bean field
-      if (seenTypeSet.contains(rawType)) {
-        String msg =
-            String.format(
-                "circular references in bean class is not allowed, but got " + 
"%s in %s",
-                rawType, seenTypeSet);
-        throw new UnsupportedOperationException(msg);
-      }
+    } else if (TypeUtils.isBean(rawType, ctx)) { // bean field
+      ctx.checkNoCycle(rawType);
       List<Field> fields =
           Descriptor.getDescriptors(rawType).stream()
               .map(
                   descriptor -> {
-                    LinkedHashSet<Class<?>> newSeenTypeSet = new 
LinkedHashSet<>(seenTypeSet);
-                    newSeenTypeSet.add(rawType);
                     String n = 
StringUtils.lowerCamelToLowerUnderscore(descriptor.getName());
-                    return inferField(n, descriptor.getTypeRef(), 
newSeenTypeSet, customTypes);
+                    TypeResolutionContext newCtx = ctx.appendTypePath(rawType);
+                    TypeRef<?> fieldType = descriptor.getTypeRef();
+                    Class<?> rawFieldType = getRawType(fieldType);
+                    if (rawType.isInterface() && rawFieldType.isInterface()) {
+                      newCtx = newCtx.withSynthesizedBeanType(rawFieldType);
+                    }
+                    return inferField(n, fieldType, newCtx);
                   })
               .collect(Collectors.toList());
       return DataTypes.structField(name, true, fields);
     } else {
       throw new UnsupportedOperationException(
           String.format(
-              "Unsupported type %s for field %s, seen type set is %s", 
typeRef, name, seenTypeSet));
+              "Unsupported type %s for field %s, seen type set is %s",
+              typeRef, name, ctx.getWalkedTypePath()));
     }
   }
 
@@ -282,12 +270,4 @@ public class TypeInference {
       Class<?> iterableType, Class<E> elementType, CustomCollectionFactory<E, 
C> factory) {
     CustomTypeEncoderRegistry.registerCustomCollection(iterableType, 
elementType, factory);
   }
-
-  private static Class<?> enclosingType(LinkedHashSet<Class<?>> newTypePath) {
-    Class<?> result = Object.class;
-    for (Class<?> type : newTypePath) {
-      result = type;
-    }
-    return result;
-  }
 }
diff --git 
a/java/fury-format/src/test/java/org/apache/fury/format/encoder/ImplementInterfaceTest.java
 
b/java/fury-format/src/test/java/org/apache/fury/format/encoder/ImplementInterfaceTest.java
new file mode 100644
index 00000000..c2b47c33
--- /dev/null
+++ 
b/java/fury-format/src/test/java/org/apache/fury/format/encoder/ImplementInterfaceTest.java
@@ -0,0 +1,127 @@
+/*
+ * 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.fury.format.encoder;
+
+import lombok.Data;
+import org.apache.fury.annotation.FuryField;
+import org.apache.fury.format.row.binary.BinaryRow;
+import org.apache.fury.memory.MemoryBuffer;
+import org.apache.fury.memory.MemoryUtils;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+public class ImplementInterfaceTest {
+
+  public interface InterfaceType {
+    int getF1();
+
+    String getF2();
+
+    NestedType getNested();
+
+    PoisonPill getPoison();
+  }
+
+  public interface NestedType {
+    @FuryField(nullable = false)
+    String getF3();
+  }
+
+  public static class PoisonPill {}
+
+  @Data
+  static class ImplementInterface implements InterfaceType {
+    public int f1;
+    public String f2;
+    public NestedType nested;
+    public PoisonPill poison = new PoisonPill();
+
+    public ImplementInterface(final int f1, final String f2) {
+      this.f1 = f1;
+      this.f2 = f2;
+    }
+  }
+
+  @Data
+  static class ImplementNestedType implements NestedType {
+    public String f3;
+
+    public ImplementNestedType(final String f3) {
+      this.f3 = f3;
+    }
+  }
+
+  static {
+    Encoders.registerCustomCodec(PoisonPill.class, new PoisonPillCodec());
+  }
+
+  @Test
+  public void testInterfaceTypes() {
+    final InterfaceType bean1 = new ImplementInterface(42, "42");
+    final RowEncoder<InterfaceType> encoder = 
Encoders.bean(InterfaceType.class);
+    final BinaryRow row = encoder.toRow(bean1);
+    final MemoryBuffer buffer = MemoryUtils.wrap(row.toBytes());
+    row.pointTo(buffer, 0, buffer.size());
+    final InterfaceType deserializedBean = encoder.fromRow(row);
+    assertEquals(bean1, deserializedBean);
+  }
+
+  @Test
+  public void testNullValue() {
+    final InterfaceType bean1 = new ImplementInterface(42, null);
+    final RowEncoder<InterfaceType> encoder = 
Encoders.bean(InterfaceType.class);
+    final BinaryRow row = encoder.toRow(bean1);
+    final MemoryBuffer buffer = MemoryUtils.wrap(row.toBytes());
+    row.pointTo(buffer, 0, buffer.size());
+    final InterfaceType deserializedBean = encoder.fromRow(row);
+    assertEquals(deserializedBean, bean1);
+  }
+
+  @Test
+  public void testNestedValue() {
+    final ImplementInterface bean1 = new ImplementInterface(42, "42");
+    bean1.nested = new ImplementNestedType("f3");
+    final RowEncoder<InterfaceType> encoder = 
Encoders.bean(InterfaceType.class);
+    final BinaryRow row = encoder.toRow(bean1);
+    final MemoryBuffer buffer = MemoryUtils.wrap(row.toBytes());
+    row.pointTo(buffer, 0, buffer.size());
+    final InterfaceType deserializedBean = encoder.fromRow(row);
+    assertEquals(bean1, deserializedBean);
+    Assert.assertEquals(bean1.nested.getF3(), 
deserializedBean.getNested().getF3());
+  }
+
+  private void assertEquals(final InterfaceType bean1, final InterfaceType 
deserializedBean) {
+    Assert.assertNotSame(deserializedBean.getClass(), bean1.getClass());
+    Assert.assertEquals(deserializedBean.getF1(), bean1.getF1());
+    Assert.assertEquals(deserializedBean.getF2(), bean1.getF2());
+  }
+
+  static class PoisonPillCodec implements 
CustomCodec.ByteArrayCodec<PoisonPill> {
+    @Override
+    public byte[] encode(final PoisonPill value) {
+      return new byte[0];
+    }
+
+    @Override
+    public PoisonPill decode(final byte[] value) {
+      throw new AssertionError();
+    }
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to