@@ -595,7 +595,8 @@ object CompileRW extends CompileRW {
595595 }
596596
597597 /** Generate RW for Scala 3 union types (A | B | C).
598- * Extracts union member types and delegates to genSealedTraitFromChildren. */
598+ * Handles the case where multiple union members share the same base class but differ by type parameters
599+ * (e.g. `Id[String] | Id[Int]`) by using full parameterized type names as discriminators. */
599600 def genUnionMacro [T : Type ](orType : Any )(using Quotes ): Expr [RW [T ]] = {
600601 import quotes .reflect ._
601602
@@ -607,26 +608,140 @@ object CompileRW extends CompileRW {
607608
608609 val memberTypes = flattenUnion(orType.asInstanceOf [TypeRepr ])
609610
610- // Generate child RW pairs — same as sealed trait children
611- val childExprs = memberTypes.map { memberType =>
611+ // Detect if any members share the same base class (type parameter collision)
612+ val simpleNames = memberTypes.map(t => getSimpleTypeNameFromType(t))
613+ val hasCollisions = simpleNames.distinct.size != simpleNames.size
614+
615+ if (hasCollisions) {
616+ genCollisionUnionMacro[T ](memberTypes)
617+ } else {
618+ // No collisions — use standard poly generation
619+ val childExprs = memberTypes.map { memberType =>
620+ memberType.asType match {
621+ case ' [t] =>
622+ val rw = Expr .summon[RW [t]].getOrElse {
623+ val childSym = memberType.typeSymbol
624+ if (childSym.isClassDef && childSym.flags.is(Flags .Case )) {
625+ genMacro[t]
626+ } else {
627+ report.errorAndAbort(s " No RW found for union member type ${memberType.show}. Ensure all union member types have an RW instance. " )
628+ }
629+ }
630+ val name = getSimpleTypeNameFromType(memberType)
631+ ' { ($ { Expr (name) }, $rw.asInstanceOf [RW [_]]) }
632+ }
633+ }
634+ val childRWsExpr = Expr .ofList(childExprs)
635+ genPolyRW[T ](childRWsExpr)
636+ }
637+ }
638+
639+ /** Generate RW for union types where multiple members share the same base class (e.g. `Id[String] | Id[Int]`).
640+ * Uses full parameterized type names as discriminators and compile-time type matching for the write path
641+ * since runtime class inspection can't distinguish erased generic variants. */
642+ private def genCollisionUnionMacro [T : Type ](memberTypes : List [Any ])(using Quotes ): Expr [RW [T ]] = {
643+ import quotes .reflect ._
644+
645+ val members = memberTypes.asInstanceOf [List [TypeRepr ]]
646+
647+ // Build child RW pairs — always generate fresh RWs (not summoned) so each gets concrete _generic info
648+ val childExprs = members.map { memberType =>
612649 memberType.asType match {
613650 case ' [t] =>
614- val rw = Expr .summon[RW [t]].getOrElse {
615- val childSym = memberType.typeSymbol
616- if (childSym.isClassDef && childSym.flags.is(Flags .Case )) {
617- genMacro[t]
618- } else {
619- report.errorAndAbort(s " No RW found for union member type ${memberType.show}. Ensure all union member types have an RW instance. " )
651+ val childSym = memberType.typeSymbol
652+ // Always generate fresh to ensure _generic reflects the concrete type args
653+ val rw = if (childSym.isClassDef && childSym.flags.is(Flags .Case )) {
654+ genMacro[t]
655+ } else {
656+ Expr .summon[RW [t]].getOrElse {
657+ report.errorAndAbort(s " No RW found for union member type ${memberType.show}. " )
620658 }
621659 }
622- val name = getSimpleTypeNameFromType(memberType)
623- ' { ($ { Expr (name) }, $rw.asInstanceOf [RW [_]]) }
660+ val simpleName = getSimpleTypeNameFromType(memberType)
661+ val fullName = fullTypeName(memberType)
662+ ' { ($ { Expr (simpleName) }, $ { Expr (fullName) }, $rw.asInstanceOf [RW [_]]) }
624663 }
625664 }
626- val childRWsExpr = Expr .ofList(childExprs)
665+ val childListExpr = Expr .ofList(childExprs)
627666
628- // Reuse the same polymorphic RW generation as sealed traits
629- genPolyRW[T ](childRWsExpr)
667+ val fullTypeNameStr = members.map(fullTypeName(_)).mkString(" | " )
668+ val fullTypeNameExpr = Expr (fullTypeNameStr)
669+
670+ ' {
671+ new RW [T ] {
672+ private val typeField = " type"
673+ private lazy val childRWs : List [(String , String , RW [_])] = $childListExpr
674+
675+ private def matchGeneric (json : Json , candidates : List [(String , String , RW [_])]): Option [(String , RW [_])] = {
676+ json match {
677+ case Obj (map) =>
678+ map.get(" _generic" ) match {
679+ case Some (genericJson) =>
680+ // Match by comparing _generic content against each candidate's definition.genericTypes
681+ candidates.find { case (_, _, rw) =>
682+ val expected = Obj (rw.definition.genericTypes.map(gt => gt.name -> gt.definition.json): _* )
683+ expected == genericJson
684+ }.map(c => (c._2, c._3))
685+ case None =>
686+ // No _generic field — take first candidate
687+ candidates.headOption.map(c => (c._2, c._3))
688+ }
689+ case _ => candidates.headOption.map(c => (c._2, c._3))
690+ }
691+ }
692+
693+ override def read (value : T ): Json = {
694+ val simpleName = safeTypeName(value)
695+ val candidates = childRWs.filter(_._1 == simpleName)
696+ // Use first candidate for read — the child RW will embed _generic in its output
697+ candidates.headOption match {
698+ case Some ((_, _, rw)) =>
699+ rw.asInstanceOf [RW [T ]].read(value) match {
700+ case obj : Obj => obj.merge(Obj (typeField -> Str (simpleName)))
701+ case other => Obj (typeField -> Str (simpleName), " value" -> other)
702+ }
703+ case None => throw RWException (s " Unknown subtype: $simpleName" )
704+ }
705+ }
706+
707+ override def write (json : Json ): T = json match {
708+ case obj @ Obj (map) =>
709+ map.get(typeField) match {
710+ case Some (Str (typeName, _)) =>
711+ val candidates = childRWs.filter(_._1 == typeName)
712+ val (_, rw) = if (candidates.size > 1 ) {
713+ // Collision — use _generic to disambiguate
714+ val cleanedJson = Obj (map - typeField)
715+ matchGeneric(cleanedJson, candidates).getOrElse(
716+ throw RWException (s " Cannot disambiguate type ' $typeName' — no matching _generic found. Available: ${candidates.map(_._2).mkString(" , " )}" )
717+ )
718+ } else {
719+ candidates.headOption.map(c => (c._2, c._3)).getOrElse(
720+ throw RWException (s " Unknown type discriminator: $typeName" )
721+ )
722+ }
723+ val cleanedMap = map - typeField
724+ val cleanedJson = if (cleanedMap.isEmpty && map.size == 2 && map.contains(" value" )) {
725+ map(" value" )
726+ } else {
727+ Obj (cleanedMap)
728+ }
729+ rw.asInstanceOf [RW [T ]].write(cleanedJson)
730+ case _ =>
731+ throw RWException (s " Missing or invalid ' $typeField' field in JSON for union type " )
732+ }
733+ case _ =>
734+ throw RWException (s " Expected JSON object for union type, got: $json" )
735+ }
736+
737+ override def definition : FabricDefinition = {
738+ val childDefs = childRWs.map { case (_, fullName, rw) =>
739+ fullName -> rw.definition
740+ }.toMap.to(VectorMap )
741+ FabricDefinition (DefType .Poly (childDefs), className = Some ($fullTypeNameExpr))
742+ }
743+ }
744+ }
630745 }
631746
632747 private def cleanFullName (name : String ): String =
@@ -799,6 +914,36 @@ object CompileRW extends CompileRW {
799914 val genericTypesExpr = generateGenericTypes(tpe)
800915 val fieldGenericNamesExpr = extractFieldGenericNames(tpe)
801916
917+ // Check if this type has type arguments (is a generic instantiation)
918+ val typeParamSyms = typeSymbol.primaryConstructor.paramSymss.headOption match {
919+ case Some (params) if params.nonEmpty && params.head.isTypeParam => params
920+ case _ => Nil
921+ }
922+ val hasTypeArgs = tpe match {
923+ case AppliedType (_, _) if typeParamSyms.nonEmpty => true
924+ case _ => false
925+ }
926+
927+ // Generate _generic JSON value at macro time
928+ val genericJsonExpr : Option [Expr [Json ]] = if (hasTypeArgs) {
929+ tpe match {
930+ case AppliedType (_, args) =>
931+ val entries = typeParamSyms.zip(args).map { case (param, arg) =>
932+ val nameExpr = Expr (param.name)
933+ arg.asType match {
934+ case ' [t] =>
935+ val rw = Expr .summon[RW [t]].getOrElse {
936+ report.errorAndAbort(s " No RW found for type parameter ${param.name} ( ${arg.show}) " )
937+ }
938+ ' { ($nameExpr, $rw.definition.json) }
939+ }
940+ }
941+ val list = Expr .ofList(entries)
942+ Some (' { Obj ($list : _* ) })
943+ case _ => None
944+ }
945+ } else None
946+
802947 ' {
803948 new ClassRW [T ] {
804949 override protected def t2Map (t : T ): Map [String , Json ] = {
@@ -807,8 +952,12 @@ object CompileRW extends CompileRW {
807952 case Some (gen) => ' { base ++ $ { gen(' {t}) } }
808953 case None => ' { base }
809954 }}
810- $ { if (hasTransient) ' { withExtra -- $transientFieldsExpr }
955+ val withTransient = $ { if (hasTransient) ' { withExtra -- $transientFieldsExpr }
811956 else ' { withExtra } }
957+ $ { genericJsonExpr match {
958+ case Some (gj) => ' { if (RW .SerializeGenerics ) withTransient + (" _generic" -> $gj) else withTransient }
959+ case None => ' { withTransient }
960+ }}
812961 }
813962
814963 override protected def map2T (map : Map [String , Json ]): T = {
0 commit comments