Skip to content

[pigeon] Empty generated Dart classes aren't serializable #160501

Closed
Yobari-Timeliners/packages
#2

Description

@feduke-nukem

Steps to reproduce

  • create pigeon/messages.dart:

  • Define sealed class A

  • Define B which extends A with some fields

  • Define C which extends A without any fields

  • run flutter pub run pigeon --input ./pigeon/messages.dart

  • get generated file src/messages.g.dart

Expected results

C.decode/C.encode is generated and we got no errors

Actual results

C.decode/C.encode was not generated, however B.decode/B.encode was.

// Autogenerated from Pigeon (v22.7.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers

import 'dart:async';
import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;

import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
import 'package:flutter/services.dart';

PlatformException _createConnectionError(String channelName) {
  return PlatformException(
    code: 'channel-error',
    message: 'Unable to establish connection on channel: "$channelName".',
  );
}

sealed class A {
}

class B extends A {
  B({
    this.number,
    this.message,
  });

  int? number;

  String? message;

  Object encode() {
    return <Object?>[
      number,
      message,
    ];
  }

  static B decode(Object result) {
    result as List<Object?>;
    return B(
      number: result[0] as int?,
      message: result[1] as String?,
    );
  }
}

class C extends A { <----- no decode/encode
}


class _PigeonCodec extends StandardMessageCodec {
  const _PigeonCodec();
  @override
  void writeValue(WriteBuffer buffer, Object? value) {
    if (value is int) {
      buffer.putUint8(4);
      buffer.putInt64(value);
    }    else if (value is B) {
      buffer.putUint8(129);
      writeValue(buffer, value.encode());
    }    else if (value is C) {
      buffer.putUint8(130);
      writeValue(buffer, value.encode()); <----- not generated, error
    } else {
      super.writeValue(buffer, value);
    }
  }

  @override
  Object? readValueOfType(int type, ReadBuffer buffer) {
    switch (type) {
      case 129: 
        return B.decode(readValue(buffer)!);
      case 130: 
        return C.decode(readValue(buffer)!); <----- not generated, error
      default:
        return super.readValueOfType(type, buffer);
    }
  }
}

class SomeHostApi {
  /// Constructor for [SomeHostApi].  The [binaryMessenger] named argument is
  /// available for dependency injection.  If it is left null, the default
  /// BinaryMessenger will be used which routes to the host platform.
  SomeHostApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
      : pigeonVar_binaryMessenger = binaryMessenger,
        pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
  final BinaryMessenger? pigeonVar_binaryMessenger;

  static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();

  final String pigeonVar_messageChannelSuffix;

  Future<C> getC(C c) async {
    final String pigeonVar_channelName = 'dev.flutter.pigeon.pigeon_sealed_plugin_example.SomeHostApi.getC$pigeonVar_messageChannelSuffix';
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
      pigeonVar_channelName,
      pigeonChannelCodec,
      binaryMessenger: pigeonVar_binaryMessenger,
    );
    final List<Object?>? pigeonVar_replyList =
        await pigeonVar_channel.send(<Object?>[c]) as List<Object?>?;
    if (pigeonVar_replyList == null) {
      throw _createConnectionError(pigeonVar_channelName);
    } else if (pigeonVar_replyList.length > 1) {
      throw PlatformException(
        code: pigeonVar_replyList[0]! as String,
        message: pigeonVar_replyList[1] as String?,
        details: pigeonVar_replyList[2],
      );
    } else if (pigeonVar_replyList[0] == null) {
      throw PlatformException(
        code: 'null-error',
        message: 'Host platform returned null value for non-null return value.',
      );
    } else {
      return (pigeonVar_replyList[0] as C?)!;
    }
  }

  Future<A> getA(A a) async {
    final String pigeonVar_channelName = 'dev.flutter.pigeon.pigeon_sealed_plugin_example.SomeHostApi.getA$pigeonVar_messageChannelSuffix';
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
      pigeonVar_channelName,
      pigeonChannelCodec,
      binaryMessenger: pigeonVar_binaryMessenger,
    );
    final List<Object?>? pigeonVar_replyList =
        await pigeonVar_channel.send(<Object?>[a]) as List<Object?>?;
    if (pigeonVar_replyList == null) {
      throw _createConnectionError(pigeonVar_channelName);
    } else if (pigeonVar_replyList.length > 1) {
      throw PlatformException(
        code: pigeonVar_replyList[0]! as String,
        message: pigeonVar_replyList[1] as String?,
        details: pigeonVar_replyList[2],
      );
    } else if (pigeonVar_replyList[0] == null) {
      throw PlatformException(
        code: 'null-error',
        message: 'Host platform returned null value for non-null return value.',
      );
    } else {
      return (pigeonVar_replyList[0] as A?)!;
    }
  }
}

class SomeFlutterApi {
  /// Constructor for [SomeFlutterApi].  The [binaryMessenger] named argument is
  /// available for dependency injection.  If it is left null, the default
  /// BinaryMessenger will be used which routes to the host platform.
  SomeFlutterApi({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''})
      : pigeonVar_binaryMessenger = binaryMessenger,
        pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
  final BinaryMessenger? pigeonVar_binaryMessenger;

  static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();

  final String pigeonVar_messageChannelSuffix;

  Future<C> getC(C c) async {
    final String pigeonVar_channelName = 'dev.flutter.pigeon.pigeon_sealed_plugin_example.SomeFlutterApi.getC$pigeonVar_messageChannelSuffix';
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
      pigeonVar_channelName,
      pigeonChannelCodec,
      binaryMessenger: pigeonVar_binaryMessenger,
    );
    final List<Object?>? pigeonVar_replyList =
        await pigeonVar_channel.send(<Object?>[c]) as List<Object?>?;
    if (pigeonVar_replyList == null) {
      throw _createConnectionError(pigeonVar_channelName);
    } else if (pigeonVar_replyList.length > 1) {
      throw PlatformException(
        code: pigeonVar_replyList[0]! as String,
        message: pigeonVar_replyList[1] as String?,
        details: pigeonVar_replyList[2],
      );
    } else if (pigeonVar_replyList[0] == null) {
      throw PlatformException(
        code: 'null-error',
        message: 'Host platform returned null value for non-null return value.',
      );
    } else {
      return (pigeonVar_replyList[0] as C?)!;
    }
  }

  Future<A> getA(A a) async {
    final String pigeonVar_channelName = 'dev.flutter.pigeon.pigeon_sealed_plugin_example.SomeFlutterApi.getA$pigeonVar_messageChannelSuffix';
    final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
      pigeonVar_channelName,
      pigeonChannelCodec,
      binaryMessenger: pigeonVar_binaryMessenger,
    );
    final List<Object?>? pigeonVar_replyList =
        await pigeonVar_channel.send(<Object?>[a]) as List<Object?>?;
    if (pigeonVar_replyList == null) {
      throw _createConnectionError(pigeonVar_channelName);
    } else if (pigeonVar_replyList.length > 1) {
      throw PlatformException(
        code: pigeonVar_replyList[0]! as String,
        message: pigeonVar_replyList[1] as String?,
        details: pigeonVar_replyList[2],
      );
    } else if (pigeonVar_replyList[0] == null) {
      throw PlatformException(
        code: 'null-error',
        message: 'Host platform returned null value for non-null return value.',
      );
    } else {
      return (pigeonVar_replyList[0] as A?)!;
    }
  }
}

Code sample

Code sample
import 'package:pigeon/pigeon.dart';

// #docregion config
@ConfigurePigeon(
  PigeonOptions(
    dartOut: 'lib/src/messages.g.dart',
    dartOptions: DartOptions(),
    kotlinOut:
        'android/src/main/kotlin/com/pigeonsealedexample/pigeon_sealed_plugin/Messages.g.kt',
    kotlinOptions:
        KotlinOptions(package: 'com.pigeonsealedexample.somepackage'),
    swiftOut: 'ios/Classes/Messages.g.swift',
    swiftOptions: SwiftOptions(),
    dartPackageName: 'pigeon_sealed_plugin_example',
  ),
)
sealed class A {
  A();
}

class B extends A {
  final int? number;
  final String? message;

  B({
    required this.number,
    this.message,
  });
}

class C extends A {
  C();
}

@HostApi()
abstract class SomeHostApi {
  C getC(C c);

  A getA(A a);
}

@HostApi()
abstract class SomeFlutterApi {
  C getC(C c);

  A getA(A a);
}

Screenshots or Video

Screenshots / Video demonstration

[Upload media here]

Logs

Logs
Deprecated. Use `dart run` instead.
Building package executable... 
Built pigeon:pigeon.

Flutter Doctor output

Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[!] Flutter (Channel master, 3.26.0-1.0.pre.168, on macOS 14.5 23F79 darwin-arm64, locale en-RU)
    ! Warning: `dart` on your path resolves to /opt/homebrew/Cellar/dart/3.5.2/libexec/bin/dart, which is not inside your current Flutter SDK checkout at /Users/feduke-nukem/Desktop/git-projects/flutter/flutter. Consider adding
      /Users/feduke-nukem/Desktop/git-projects/flutter/flutter/bin to the front of your path.
    ! Upstream repository https://github.com/feduke-nukem/flutter is not a standard remote.
      Set environment variable "FLUTTER_GIT_URL" to https://github.com/feduke-nukem/flutter to dismiss this error.
[!] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 15.4)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2023.3)
[✓] VS Code (version 1.96.0)
[✓] Connected device (6 available)
    ! Error: Browsing on the local area network for iPhone (Анна). Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources

Activity

  1. feduke-nukem commented on Dec 18, 2024

    @feduke-nukem
    Author
  2. feduke-nukem commented on Dec 18, 2024

    @feduke-nukem
    ContributorAuthor

    Relates to #153995

  3. stuartmorgan-g commented on Dec 18, 2024

    @stuartmorgan-g
    Contributor

    Please do not ping individual people when filling issues; we have a triage process.

  4. feduke-nukem commented on Dec 18, 2024

    @feduke-nukem
    Author
  5. darshankawar commented on Dec 19, 2024

    @darshankawar
    Member

    I was able to replicate the reported behavior, wherein the generated messages.g.dart file shows below:

    Screenshot 2024-12-19 at 12 23 54 PM Screenshot 2024-12-19 at 12 24 12 PM
    stable : 3.27.1
    master : 3.28.0-1.0.pre.87
    
    
  6. added
    packageflutter/packages repository. See also p: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    p: pigeonrelated to pigeon messaging codegen tool
    and removed
    in triagePresently being triaged by the triage team
    on Dec 19, 2024
  7. tarrinneal commented on Dec 19, 2024

    @tarrinneal
    Contributor

    This is actually a result of the class having no fields.

    Just for my understanding and to guide future work, what is the use case of having the empty class?

  8. feduke-nukem commented on Dec 20, 2024

    @feduke-nukem
    ContributorAuthor

    This is actually a result of the class having no fields.

    Just for my understanding and to guide future work, what is the use case of having the empty class?

    Well, first of all, Kotlin and Swift do not have such problem:

    sealed class A 
    /** Generated class from Pigeon that represents data sent in messages. */
    data class B (
      val number: Long? = null,
      val message: String? = null
    ) : A()
     {
      companion object {
        fun fromList(pigeonVar_list: List<Any?>): B {
          val number = pigeonVar_list[0] as Long?
          val message = pigeonVar_list[1] as String?
          return B(number, message)
        }
      }
      fun toList(): List<Any?> {
        return listOf(
          number,
          message,
        )
      }
    }
    
    /** Generated class from Pigeon that represents data sent in messages. */
    data class C (
    ) : A()
     {
      companion object {
        fun fromList(pigeonVar_list: List<Any?>): C {
          return C()
        }
      }
      fun toList(): List<Any?> {
        return listOf(
        )
      }
    }
    private open class MessagesPigeonCodec : StandardMessageCodec() {
      override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
        return when (type) {
          129.toByte() -> {
            return (readValue(buffer) as? List<Any?>)?.let {
              B.fromList(it)
            }
          }
          130.toByte() -> {
            return (readValue(buffer) as? List<Any?>)?.let {
              C.fromList(it)
            }
          }
          else -> super.readValueOfType(type, buffer)
        }
      }
      override fun writeValue(stream: ByteArrayOutputStream, value: Any?)   {
        when (value) {
          is B -> {
            stream.write(129)
            writeValue(stream, value.toList())
          }
          is C -> {
            stream.write(130)
            writeValue(stream, value.toList())
          }
          else -> super.writeValue(stream, value)
        }
      }
    }
    protocol A {
    
    }
    
    /// Generated class from Pigeon that represents data sent in messages.
    struct B: A {
      var number: Int64? = nil
      var message: String? = nil
    
    
      // swift-format-ignore: AlwaysUseLowerCamelCase
      static func fromList(_ pigeonVar_list: [Any?]) -> B? {
        let number: Int64? = nilOrValue(pigeonVar_list[0])
        let message: String? = nilOrValue(pigeonVar_list[1])
    
        return B(
          number: number,
          message: message
        )
      }
      func toList() -> [Any?] {
        return [
          number,
          message,
        ]
      }
    }
    
    /// Generated class from Pigeon that represents data sent in messages.
    struct C: A {
    
    
      // swift-format-ignore: AlwaysUseLowerCamelCase
      static func fromList(_ pigeonVar_list: [Any?]) -> C? {
    
        return C(
        )
      }
      func toList() -> [Any?] {
        return [
        ]
      }
    }

    Secondly, the typical use case for sealed classes are events/states so let's image we are implementing custom video player. I would definitely have (especially with EventChannel support) something like:

    sealed class VideoPlayerEvent {}
    
    class VideoPlayerStarted extends VideoPlayerEvent {}
    
    class VideoPlayerPaused extends VideoPlayerEvent {}
    
    class VideoPlayerStopped extends VideoPlayerEvent {}
    
    class VideoPlayerResumed extends VideoPlayerEvent {}
    
    class VideoPlayerSeek extends VideoPlayerEvent {
      final int positionUs;
    
      VideoPlayerSeek({required this.positionUs});
    }

    As you can see - I don't need properties/fields for all of these classes.

    In my opinion, described behaviour in this issue is unexpected and should not limit user for specific implementations. Even if I want all my classes to have no fields - it should work as it works for Kotlin/Swift.

    If it is expected behaviour - Pigeon should throw exception with verbose description of a reason why user shouldn't generate empty classes (can't imagine such reason)

  9. added
    P2Important issues not at the top of the work list
    on Dec 22, 2024
  10. changed the title [-][pigeon] No encode/decode generated dart class (22.7.0 version, sealed class)[/-] [+][pigeon] Empty generated Dart classes aren't serializable[/+] on Dec 22, 2024
  11. added
    r: fixedIssue is closed as already fixed in a newer version
    on Jan 31, 2025
  12. github-actions commented on Feb 14, 2025

    @github-actions

    This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of flutter doctor -v and a minimal reproduction of the issue.

  13. locked as resolved and limited conversation to collaborators on Feb 14, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listfound in release: 3.27Found to occur in 3.27found in release: 3.28Found to occur in 3.28has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: pigeonrelated to pigeon messaging codegen toolpackageflutter/packages repository. See also p: labels.r: fixedIssue is closed as already fixed in a newer versionteam-ecosystemOwned by Ecosystem teamtriaged-ecosystemTriaged by Ecosystem team

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions