Skip to content

BorshSchema derive macro creates recursive types when enum variant names match type names #355

@ghost

Description

Description

The BorshSchema derive macro generates incorrect code that causes infinite recursion errors when an enum variant contains a type with a similar name to the variant itself. This occurs even when there is no actual circular dependency in the user's type definitions.

Steps to Reproduce

use borsh::BorshSchema;

#[derive(BorshSchema)]
pub struct AuthDataV3 {
    pub field: u32,
}

#[derive(BorshSchema)]
pub struct AuthDataV4 {
    pub field: u32,
}

#[derive(BorshSchema)]
pub enum AuthData {
    V3(AuthDataV3),  // Variant V3 contains type AuthDataV3
    V4(AuthDataV4),  // Variant V4 contains type AuthDataV4
}

Expected Behavior

The code should compile successfully since there are no actual circular dependencies between the types.

Actual Behavior

Compilation fails with the following errors:

error[E0072]: recursive type `<AuthData as BorshSchema>::add_definitions_recursively::AuthDataV3` has infinite size
 --> src/lib.rs:13:10
  |
  | #[derive(BorshSchema)]
  |          ^^^^^^^^^^^
  | pub enum AuthData {
  |     V3(AuthDataV3),
  |        ---------- recursive without indirection

error[E0072]: recursive type `<AuthData as BorshSchema>::add_definitions_recursively::AuthDataV4` has infinite size
 --> src/lib.rs:13:10
  |
  | #[derive(BorshSchema)]
  |          ^^^^^^^^^^^
  | pub enum AuthData {
  |     V4(AuthDataV4),
  |        ---------- recursive without indirection

Root Cause

When expanding the macro with cargo expand, the issue becomes clear. The BorshSchema implementation for the enum creates local wrapper structs with the same names as the contained types:

impl borsh::BorshSchema for AuthData {
    fn add_definitions_recursively(...) {
        // Creates a LOCAL struct that shadows the actual AuthDataV3 type
        struct AuthDataV3(AuthDataV3);
        impl borsh::BorshSchema for AuthDataV3 {
            fn add_definitions_recursively(...) {
                // This references itself recursively!
                let fields = borsh::schema::Fields::UnnamedFields(
                    vec![<AuthDataV3 as borsh::BorshSchema>::declaration()]
                );
                // ...
                <AuthDataV3 as borsh::BorshSchema>::add_definitions_recursively(definitions);
            }
        }
        
        // Same issue for V4
        struct AuthDataV4(AuthDataV4);
        // ...
    }
}

The macro creates wrapper structs struct AuthDataV3(AuthDataV3) and struct AuthDataV4(AuthDataV4) inside the add_definitions_recursively method. These wrapper structs shadow the actual types and create infinite recursion because they reference themselves.

Workaround

Users can work around this issue by:

  1. Renaming the struct types to avoid the naming collision:
#[derive(BorshSchema)]
pub struct AuthDataV3Struct {  // Renamed
    pub field: u32,
}

#[derive(BorshSchema)]
pub enum AuthData {
    V3(AuthDataV3Struct),  // No naming collision
}
  1. Using type aliases:
pub type AuthV3 = AuthDataV3;

#[derive(BorshSchema)]
pub enum AuthData {
    V3(AuthV3),  // Uses alias instead
}
  1. Not deriving BorshSchema for the enum (only using BorshSerialize/BorshDeserialize)

Suggested Fix

The macro should generate wrapper structs with unique names that don't shadow user-defined types. For example, it could prefix or suffix the generated struct names:

// Instead of: struct AuthDataV3(AuthDataV3);
// Generate:   struct __BorshSchemaAuthDataV3Wrapper(AuthDataV3);

Additional Context

This issue only affects the BorshSchema derive macro. The BorshSerialize and BorshDeserialize derive macros work correctly with the same type definitions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions