From 55323fc4f79d4007b4803d57ce45cb07e0a151b9 Mon Sep 17 00:00:00 2001 From: adaroussin Date: Thu, 6 Nov 2025 14:39:05 +0100 Subject: [PATCH] feat: add serializers for mutable collections: ArrayDeque, ArrayBuffer, HashMap, Queue, HashSet --- .../MutableArrayDequeSerializer.scala | 85 ++++++++++++++ .../serializer/MutableBufferSerializer.scala | 89 +++++++++++++++ .../api/serializer/MutableMapSerializer.scala | 106 ++++++++++++++++++ .../serializer/MutableQueueSerializer.scala | 85 ++++++++++++++ .../api/serializer/MutableSetSerializer.scala | 72 ++++++++++++ .../org/apache/flinkx/api/serializers.scala | 68 +++++++++++ .../apache/flinkx/api/SerializerTest.scala | 49 ++++++++ 7 files changed, 554 insertions(+) create mode 100644 modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableArrayDequeSerializer.scala create mode 100644 modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableBufferSerializer.scala create mode 100644 modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableMapSerializer.scala create mode 100644 modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableQueueSerializer.scala create mode 100644 modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableSetSerializer.scala diff --git a/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableArrayDequeSerializer.scala b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableArrayDequeSerializer.scala new file mode 100644 index 0000000..eb35d70 --- /dev/null +++ b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableArrayDequeSerializer.scala @@ -0,0 +1,85 @@ +package org.apache.flinkx.api.serializer + +import org.apache.flink.api.common.typeutils.{TypeSerializer, TypeSerializerSnapshot} +import org.apache.flink.core.memory.{DataInputView, DataOutputView} +import org.apache.flinkx.api.{NullMarker, VariableLengthDataType} + +import scala.collection.mutable + +/** Serializer for [[mutable.ArrayDeque]]. Handle nullable value. */ +class MutableArrayDequeSerializer[A](child: TypeSerializer[A], clazz: Class[A]) + extends MutableSerializer[mutable.ArrayDeque[A]] { + + override def copy(from: mutable.ArrayDeque[A]): mutable.ArrayDeque[A] = + if (from == null) { + from + } else { + val length = from.length + val result = from.clone() + if (!child.isImmutableType) { + var i = 0 + while (i < length) { + val element = result(i) + if (element != null) result(i) = child.copy(element) + i += 1 + } + } + result + } + + override def duplicate(): MutableArrayDequeSerializer[A] = { + val duplicatedChild = child.duplicate() + if (duplicatedChild.eq(child)) { + this + } else { + new MutableArrayDequeSerializer[A](duplicatedChild, clazz) + } + } + + override def createInstance(): mutable.ArrayDeque[A] = mutable.ArrayDeque.empty[A] + + override def getLength: Int = VariableLengthDataType + + override def serialize(records: mutable.ArrayDeque[A], target: DataOutputView): Unit = + if (records == null) { + target.writeInt(NullMarker) + } else { + target.writeInt(records.length) + var i = 0 + while (i < records.length) { // while loop is significantly faster than foreach when working on arrays + child.serialize(records(i), target) + i += 1 + } + } + + override def deserialize(source: DataInputView): mutable.ArrayDeque[A] = { + var remaining = source.readInt() + if (remaining == NullMarker) { + null + } else { + val arrayDeque = createInstance() + while (remaining > 0) { + arrayDeque.append(child.deserialize(source)) + remaining -= 1 + } + arrayDeque + } + } + + override def copy(source: DataInputView, target: DataOutputView): Unit = { + var remaining = source.readInt() + target.writeInt(remaining) + while (remaining > 0) { + child.copy(source, target) + remaining -= 1 + } + } + + override def snapshotConfiguration(): TypeSerializerSnapshot[mutable.ArrayDeque[A]] = + new CollectionSerializerSnapshot[mutable.ArrayDeque, A, MutableArrayDequeSerializer[A]]( + child, + classOf[MutableArrayDequeSerializer[A]], + clazz + ) + +} diff --git a/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableBufferSerializer.scala b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableBufferSerializer.scala new file mode 100644 index 0000000..db677ea --- /dev/null +++ b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableBufferSerializer.scala @@ -0,0 +1,89 @@ +package org.apache.flinkx.api.serializer + +import org.apache.flink.api.common.typeutils.{TypeSerializer, TypeSerializerSnapshot} +import org.apache.flink.core.memory.{DataInputView, DataOutputView} +import org.apache.flinkx.api.{NullMarker, VariableLengthDataType} + +import scala.collection.mutable + +/** Serializer for [[mutable.Buffer]]. Handle nullable value. */ +class MutableBufferSerializer[A](child: TypeSerializer[A], clazz: Class[A]) + extends MutableSerializer[mutable.Buffer[A]] { + + override def copy(from: mutable.Buffer[A]): mutable.Buffer[A] = + if (from == null) { + from + } else { + val length = from.length + val result = from.clone() + if (!child.isImmutableType) { + var i = 0 + while (i < length) { + val element = result(i) + if (element != null) result(i) = child.copy(element) + i += 1 + } + } + result + } + + override def duplicate(): MutableBufferSerializer[A] = { + val duplicatedChild = child.duplicate() + if (duplicatedChild.eq(child)) { + this + } else { + new MutableBufferSerializer[A](duplicatedChild, clazz) + } + } + + override def createInstance(): mutable.Buffer[A] = mutable.Buffer.empty[A] + + override def getLength: Int = VariableLengthDataType + + override def serialize(records: mutable.Buffer[A], target: DataOutputView): Unit = + if (records == null) { + target.writeInt(NullMarker) + } else { + target.writeInt(records.length) + records match { + case _: mutable.ArrayBuffer[_] | _: mutable.ArrayDeque[_] => + var i = 0 + while (i < records.length) { // while loop is significantly faster than foreach when working on arrays + child.serialize(records(i), target) + i += 1 + } + case _ => records.foreach(element => child.serialize(element, target)) + } + } + + override def deserialize(source: DataInputView): mutable.Buffer[A] = { + var remaining = source.readInt() + if (remaining == NullMarker) { + null + } else { + val buffer = createInstance() + while (remaining > 0) { + buffer.append(child.deserialize(source)) + remaining -= 1 + } + buffer + } + } + + override def copy(source: DataInputView, target: DataOutputView): Unit = { + var remaining = source.readInt() + target.writeInt(remaining) + while (remaining > 0) { + child.copy(source, target) + remaining -= 1 + } + } + + override def snapshotConfiguration(): TypeSerializerSnapshot[mutable.Buffer[A]] = + new CollectionSerializerSnapshot[mutable.Buffer, A, MutableBufferSerializer[A]]( + child, + classOf[MutableBufferSerializer[A]], + clazz + ) + +} diff --git a/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableMapSerializer.scala b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableMapSerializer.scala new file mode 100644 index 0000000..ab2d146 --- /dev/null +++ b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableMapSerializer.scala @@ -0,0 +1,106 @@ +package org.apache.flinkx.api.serializer + +import org.apache.flink.api.common.typeutils.{TypeSerializer, TypeSerializerSchemaCompatibility, TypeSerializerSnapshot} +import org.apache.flink.core.memory.{DataInputView, DataOutputView} +import org.apache.flinkx.api.{NullMarker, VariableLengthDataType} + +import scala.collection.mutable + +/** Serializer for [[mutable.Map]]. Handle nullable value. */ +class MutableMapSerializer[K, V]( + keySerializer: TypeSerializer[K], + valueSerializer: TypeSerializer[V] +) extends MutableSerializer[mutable.Map[K, V]] { + + override def copy(from: mutable.Map[K, V]): mutable.Map[K, V] = + if (from == null) { + from + } else { + from.map(element => (keySerializer.copy(element._1), valueSerializer.copy(element._2))) + } + + override def duplicate(): MutableMapSerializer[K, V] = { + val duplicatedKs = keySerializer.duplicate() + val duplicatedVs = valueSerializer.duplicate() + if (duplicatedKs.eq(keySerializer) && duplicatedVs.eq(valueSerializer)) { + this + } else { + new MutableMapSerializer(duplicatedKs, duplicatedVs) + } + } + + override def createInstance(): mutable.Map[K, V] = mutable.Map.empty[K, V] + + override def getLength: Int = VariableLengthDataType + + override def serialize(records: mutable.Map[K, V], target: DataOutputView): Unit = + if (records == null) { + target.writeInt(NullMarker) + } else { + target.writeInt(records.size) + records.foreach(element => { + keySerializer.serialize(element._1, target) + valueSerializer.serialize(element._2, target) + }) + } + + override def deserialize(source: DataInputView): mutable.Map[K, V] = { + var remaining = source.readInt() // The valid range of actual data is >= 0. Only markers are negative + if (remaining == NullMarker) { + null + } else { + val map = createInstance() + while (remaining > 0) { + val key = keySerializer.deserialize(source) + val value = valueSerializer.deserialize(source) + map.put(key, value) + remaining -= 1 + } + map + } + } + + override def copy(source: DataInputView, target: DataOutputView): Unit = { + var remaining = source.readInt() + target.writeInt(remaining) + while (remaining > 0) { + keySerializer.copy(source, target) + valueSerializer.copy(source, target) + remaining -= 1 + } + } + + override def snapshotConfiguration(): TypeSerializerSnapshot[mutable.Map[K, V]] = + new MutableMapSerializerSnapshot(keySerializer, valueSerializer) + +} + +class MutableMapSerializerSnapshot[K, V]( + private var keySerializer: TypeSerializer[K], + private var valueSerializer: TypeSerializer[V] +) extends TypeSerializerSnapshot[mutable.Map[K, V]] { + + def this() = this(null, null) + + override def getCurrentVersion: Int = 1 + + override def writeSnapshot(out: DataOutputView): Unit = { + TypeSerializerSnapshot.writeVersionedSnapshot(out, keySerializer.snapshotConfiguration()) + TypeSerializerSnapshot.writeVersionedSnapshot(out, valueSerializer.snapshotConfiguration()) + } + + override def readSnapshot(readVersion: Int, in: DataInputView, userCodeClassLoader: ClassLoader): Unit = { + keySerializer = TypeSerializerSnapshot.readVersionedSnapshot[K](in, userCodeClassLoader).restoreSerializer() + valueSerializer = TypeSerializerSnapshot.readVersionedSnapshot[V](in, userCodeClassLoader).restoreSerializer() + } + + override def resolveSchemaCompatibility( + oldSerializerSnapshot: TypeSerializerSnapshot[mutable.Map[K, V]] + ): TypeSerializerSchemaCompatibility[mutable.Map[K, V]] = { + TypeSerializerSchemaCompatibility.compatibleAsIs() + } + + override def restoreSerializer(): TypeSerializer[mutable.Map[K, V]] = + new MutableMapSerializer(keySerializer, valueSerializer) + +} diff --git a/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableQueueSerializer.scala b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableQueueSerializer.scala new file mode 100644 index 0000000..c5d2126 --- /dev/null +++ b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableQueueSerializer.scala @@ -0,0 +1,85 @@ +package org.apache.flinkx.api.serializer + +import org.apache.flink.api.common.typeutils.{TypeSerializer, TypeSerializerSnapshot} +import org.apache.flink.core.memory.{DataInputView, DataOutputView} +import org.apache.flinkx.api.{NullMarker, VariableLengthDataType} + +import scala.collection.mutable + +/** Serializer for [[mutable.Queue]]. Handle nullable value. */ +class MutableQueueSerializer[A](child: TypeSerializer[A], clazz: Class[A]) extends MutableSerializer[mutable.Queue[A]] { + + override def copy(from: mutable.Queue[A]): mutable.Queue[A] = + if (from == null) { + from + } else { + val length = from.length + val result = from.clone() + if (!child.isImmutableType) { + var i = 0 + while (i < length) { + val element = result(i) + if (element != null) result(i) = child.copy(element) + i += 1 + } + } + result + } + + override def duplicate(): MutableQueueSerializer[A] = { + val duplicatedChild = child.duplicate() + if (duplicatedChild.eq(child)) { + this + } else { + new MutableQueueSerializer[A](duplicatedChild, clazz) + } + } + + override def createInstance(): mutable.Queue[A] = mutable.Queue.empty[A] + + override def getLength: Int = VariableLengthDataType + + override def serialize(records: mutable.Queue[A], target: DataOutputView): Unit = + if (records == null) { + target.writeInt(NullMarker) + } else { + target.writeInt(records.length) + var i = 0 + while (i < records.length) { // while loop is significantly faster than foreach when working on arrays + child.serialize(records(i), target) + i += 1 + } + } + + override def deserialize(source: DataInputView): mutable.Queue[A] = { + var remaining = source.readInt() + if (remaining == NullMarker) { + null + } else { + val queue = createInstance() + while (remaining > 0) { + val a = child.deserialize(source) + queue.append(a) + remaining -= 1 + } + queue + } + } + + override def copy(source: DataInputView, target: DataOutputView): Unit = { + var remaining = source.readInt() + target.writeInt(remaining) + while (remaining > 0) { + child.copy(source, target) + remaining -= 1 + } + } + + override def snapshotConfiguration(): TypeSerializerSnapshot[mutable.Queue[A]] = + new CollectionSerializerSnapshot[mutable.Queue, A, MutableQueueSerializer[A]]( + child, + classOf[MutableQueueSerializer[A]], + clazz + ) + +} diff --git a/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableSetSerializer.scala b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableSetSerializer.scala new file mode 100644 index 0000000..a40170a --- /dev/null +++ b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializer/MutableSetSerializer.scala @@ -0,0 +1,72 @@ +package org.apache.flinkx.api.serializer + +import org.apache.flink.api.common.typeutils.{TypeSerializer, TypeSerializerSnapshot} +import org.apache.flink.core.memory.{DataInputView, DataOutputView} +import org.apache.flinkx.api.{NullMarker, VariableLengthDataType} + +import scala.collection.mutable + +/** Serializer for [[mutable.Set]]. Handle nullable value. */ +class MutableSetSerializer[A](child: TypeSerializer[A], clazz: Class[A]) extends MutableSerializer[mutable.Set[A]] { + + override def copy(from: mutable.Set[A]): mutable.Set[A] = + if (from == null) { + from + } else if (child.isImmutableType) { + from.clone() + } else { + from.map(child.copy) + } + + override def duplicate(): MutableSetSerializer[A] = { + val duplicatedChild = child.duplicate() + if (duplicatedChild.eq(child)) { + this + } else { + new MutableSetSerializer[A](duplicatedChild, clazz) + } + } + + override def createInstance(): mutable.Set[A] = mutable.Set.empty[A] + + override def getLength: Int = VariableLengthDataType + + override def serialize(records: mutable.Set[A], target: DataOutputView): Unit = + if (records == null) { + target.writeInt(NullMarker) + } else { + target.writeInt(records.size) + records.foreach(element => child.serialize(element, target)) + } + + override def deserialize(source: DataInputView): mutable.Set[A] = { + var remaining = source.readInt() + if (remaining == NullMarker) { + null + } else { + val set = createInstance() + while (remaining > 0) { + set.add(child.deserialize(source)) + remaining -= 1 + } + set + } + } + + override def copy(source: DataInputView, target: DataOutputView): Unit = { + var remaining = source.readInt() + target.writeInt(remaining) + while (remaining > 0) { + child.copy(source, target) + remaining -= 1 + } + } + + override def snapshotConfiguration(): TypeSerializerSnapshot[mutable.Set[A]] = + new CollectionSerializerSnapshot[mutable.Set, A, MutableSetSerializer[A]]( + child, + classOf[MutableSetSerializer[A]], + clazz + ) + +} diff --git a/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializers.scala b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializers.scala index b1a0b47..22f6ac2 100644 --- a/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializers.scala +++ b/modules/flink-common-api/src/main/scala/org/apache/flinkx/api/serializers.scala @@ -34,6 +34,7 @@ import java.math.{BigDecimal => JBigDecimal} import java.time._ import java.util.UUID import scala.collection.immutable.{SortedSet, TreeSet} +import scala.collection.mutable import scala.concurrent.duration.{Duration, FiniteDuration, TimeUnit} import scala.reflect.{ClassTag, classTag} @@ -192,6 +193,73 @@ trait serializers extends LowPrioImplicits { ): TypeInformation[Either[A, B]] = new EitherTypeInfo(tag.runtimeClass.asInstanceOf[Class[Either[A, B]]], a, b) + implicit def mutableArrayDequeInfo[A: ClassTag](implicit + as: TypeSerializer[A] + ): TypeInformation[mutable.ArrayDeque[A]] = { + implicit val mutableArrayDequeSerializer: MutableArrayDequeSerializer[A] = + new MutableArrayDequeSerializer[A](as, classTag[A].runtimeClass.asInstanceOf[Class[A]]) + SimpleTypeInfo[mutable.ArrayDeque[A]](3, 3) + } + + implicit def mutableBufferInfo[A: ClassTag](implicit + as: TypeSerializer[A] + ): TypeInformation[mutable.Buffer[A]] = { + implicit val mutableBufferSerializer: MutableBufferSerializer[A] = + new MutableBufferSerializer[A](as, classTag[A].runtimeClass.asInstanceOf[Class[A]]) + SimpleTypeInfo[mutable.Buffer[A]](0) // Traits don't have fields + } + + implicit def mutableArrayBufferInfo[A: ClassTag](implicit + as: TypeSerializer[A] + ): TypeInformation[mutable.ArrayBuffer[A]] = { + implicit val mutableBufferSerializer: TypeSerializer[mutable.ArrayBuffer[A]] = + new MutableBufferSerializer[A](as, classTag[A].runtimeClass.asInstanceOf[Class[A]]) + .asInstanceOf[TypeSerializer[mutable.ArrayBuffer[A]]] + SimpleTypeInfo[mutable.ArrayBuffer[A]](3, 3) + } + + implicit def mutableQueueInfo[A: ClassTag](implicit + as: TypeSerializer[A] + ): TypeInformation[mutable.Queue[A]] = { + implicit val mutableQueueSerializer: MutableQueueSerializer[A] = + new MutableQueueSerializer[A](as, classTag[A].runtimeClass.asInstanceOf[Class[A]]) + SimpleTypeInfo[mutable.Queue[A]](3, 3) + } + + implicit def mutableMapInfo[K, V](implicit + ks: TypeSerializer[K], + vs: TypeSerializer[V] + ): TypeInformation[mutable.Map[K, V]] = { + implicit val mutableMapSerializer: MutableMapSerializer[K, V] = new MutableMapSerializer[K, V](ks, vs) + SimpleTypeInfo[mutable.Map[K, V]](0) // Traits don't have fields + } + + implicit def mutableHashMapInfo[K, V](implicit + ks: TypeSerializer[K], + vs: TypeSerializer[V] + ): TypeInformation[mutable.HashMap[K, V]] = { + implicit val mutableMapSerializer: TypeSerializer[mutable.HashMap[K, V]] = new MutableMapSerializer[K, V](ks, vs) + .asInstanceOf[TypeSerializer[mutable.HashMap[K, V]]] + SimpleTypeInfo[mutable.HashMap[K, V]](4, 4) + } + + implicit def mutableSetInfo[A: ClassTag](implicit + as: TypeSerializer[A] + ): TypeInformation[mutable.Set[A]] = { + implicit val mutableSetSerializer: MutableSetSerializer[A] = + new MutableSetSerializer[A](as, classTag[A].runtimeClass.asInstanceOf[Class[A]]) + SimpleTypeInfo[mutable.Set[A]](0) // Traits don't have fields + } + + implicit def mutableHashSetInfo[A: ClassTag](implicit + as: TypeSerializer[A] + ): TypeInformation[mutable.HashSet[A]] = { + implicit val mutableHashSetSerializer: TypeSerializer[mutable.HashSet[A]] = + new MutableSetSerializer[A](as, classTag[A].runtimeClass.asInstanceOf[Class[A]]) + .asInstanceOf[TypeSerializer[mutable.HashSet[A]]] + SimpleTypeInfo[mutable.HashSet[A]](4, 4) + } + /** Create a [[TypeInformation]] of `SortedSet[A]`. Given the fact ordering used by the `SortedSet` cannot be known, * the `TypeInformation` of its ordering has to be available in the context. * @param as diff --git a/modules/flink-common-api/src/test/scala/org/apache/flinkx/api/SerializerTest.scala b/modules/flink-common-api/src/test/scala/org/apache/flinkx/api/SerializerTest.scala index 66c4865..e5e8930 100644 --- a/modules/flink-common-api/src/test/scala/org/apache/flinkx/api/SerializerTest.scala +++ b/modules/flink-common-api/src/test/scala/org/apache/flinkx/api/SerializerTest.scala @@ -17,6 +17,7 @@ import org.scalatest.matchers.should.Matchers import java.time._ import java.util.UUID import scala.collection.immutable.{BitSet, SortedSet, TreeSet} +import scala.collection.mutable import scala.concurrent.duration.Duration class SerializerTest extends AnyFlatSpec with Matchers with Inspectors with TestUtils { @@ -253,6 +254,54 @@ class SerializerTest extends AnyFlatSpec with Matchers with Inspectors with Test javaSerializable(ser) } + it should "serialize mutable.ArrayDeque" in { + testTypeInfoAndSerializer(mutable.ArrayDeque("1", "2", "3")) + } + + it should "serialize mutable.Buffer" in { + testTypeInfoAndSerializer(mutable.Buffer("1", "2", "3")) + } + + it should "serialize mutable.ArrayBuffer (default implementation of mutable.Buffer)" in { + testTypeInfoAndSerializer(mutable.ArrayBuffer("1", "2", "3")) + } + + it should "serialize mutable.ListBuffer (a different implementation than the mutable.Buffer default)" in { + testTypeInfoAndSerializer[mutable.Buffer[String]](mutable.ListBuffer("1", "2", "3")) + } + + it should "serialize mutable.Queue" in { + testTypeInfoAndSerializer(mutable.Queue("1", "2", "3")) + } + + it should "serialize mutable.Set" in { + testTypeInfoAndSerializer(mutable.Set("1", "2", "3")) + } + + it should "serialize mutable.HashSet (default implementation of mutable.Set)" in { + testTypeInfoAndSerializer(mutable.HashSet("1", "2", "3")) + } + + it should "serialize mutable.LinkedHashSet (a different implementation than the mutable.Set default)" in { + testTypeInfoAndSerializer[mutable.Set[String]](mutable.LinkedHashSet("1", "2", "3")) + } + + it should "serialize mutable.Map of Int" in { + testTypeInfoAndSerializer(mutable.Map(1 -> 9, 2 -> 8, 3 -> 7)) + } + + it should "serialize mutable.Map" in { + testTypeInfoAndSerializer(mutable.Map("1" -> "a", "2" -> "b", "3" -> "c")) + } + + it should "serialize mutable.HashMap (default implementation of mutable.Map)" in { + testTypeInfoAndSerializer(mutable.HashMap("1" -> "a", "2" -> "b", "3" -> "c")) + } + + it should "serialize mutable.LinkedHashMap (a different implementation than the mutable.Map default)" in { + testTypeInfoAndSerializer[mutable.Map[String, String]](mutable.LinkedHashMap("1" -> "a", "2" -> "b", "3" -> "c")) + } + it should "serialize SortedSet of Unit" in { testTypeInfoAndSerializer[SortedSet[Unit]](SortedSet((), ())) }