You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The purpose of this task is mostly for you to realize how poor the Jackson serialization is in Akka, and what Jackson's problems this project is supposed to alleviate.
That's a test suite from my current project that strives to capture all the problems discovered with Jackson serializer so far.
packagemyproject.archunitimportjava.lang.reflect.ParameterizedTypeimportjava.lang.reflect.Typeimportscala.jdk.CollectionConverters.asScalaSetConverterimportcom.fasterxml.jackson.annotation.JsonTypeInfoimportcom.fasterxml.jackson.databind.annotation.JsonDeserializeimportcom.fasterxml.jackson.databind.annotation.JsonSerializeimportcom.tngtech.archunit.base.DescribedPredicateimportcom.tngtech.archunit.core.domain._importcom.tngtech.archunit.lang.ArchConditionimportcom.tngtech.archunit.lang.ConditionEventsimportcom.tngtech.archunit.lang.SimpleConditionEventimportcom.tngtech.archunit.lang.syntax.ArchRuleDefinition.classesimportcom.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClassesimportorg.scalatest.wordspec.AnyWordSpecLikeimportmyproject.CborSerializableimportmyproject.archunit.BaseArchUnitSpec.importedProductionClassesimportmyproject.core.ScalaObjectUtils._importmyproject.core.serialization.EnumEntryDeserializerimportmyproject.core.serialization.EnumEntrySerializer// TODO (GMV3-965): maybe we should extract ArchUnit as separate service with one method for each of those cases// and plug in to this test suite to verify whole code base,// but additionally add a Spec that for each method will provide some problematic classes// and thus we will check the checker service itselfclassJacksonSerializationArchUnitSpecextendsAnyWordSpecLikewithBaseArchUnitSpec {
implicitclassJavaClassJacksonOps(self: JavaClass) {
defhasExplicitJacksonAnnotations:Boolean= {
self.hasAnnotation[JsonSerialize] && self.hasAnnotation[JsonDeserialize] ||
self.hasAnnotation[JsonTypeInfo]
}
}
// The below tests (roughly) check that the classes used as messages/events/state in Akka// are always marked as CborSerializable, to ensure that Jackson CBOR and not legacy Java serialization// is used for their serialization.// For Akka to ensure that condition statically, a major redesign would be necessary -// all methods like `ask`, `tell`, `Effect.persist` etc. would need to require an implicit `Codec` (?) parameter.// The below tests do NOT ensure that the Jackson serialization of messages/events/state will actually succeed in the runtime."Messages, events and entity state classes" should {
"implement CborSerializable" in {
classes
.should(newArchCondition[JavaClass]("only use CborSerializable message/event/state types") {
overridedefcheck(clazz: JavaClass, events: ConditionEvents):Unit= {
clazz.getAllMethods.asScala.foreach { method =>defcheckType(tpe: Type, category: String, failsWhen: String):Unit= {
tpe match {
caseclazz: Class[_] if clazz.getPackageName.startsWith("akka") =>// OK, acceptablecaseclazz: Class[_] if clazz ==classOf[scala.Nothing] =>// OK, acceptablecaseclazz: Class[_] if!classOf[CborSerializable].isAssignableFrom(clazz) =>valmessage=s"Type ${clazz.getName} is used as Akka $category (as observed in the return type of method ${method.getFullName}), "+s"but does NOT extend CborSerializable; this will fail in the runtime $failsWhen"
events.add(SimpleConditionEvent.violated(clazz, message))
case _ =>
}
}
valreturnType= method.getRawReturnType
valgenericReturnType= method.reflect.getGenericReturnType
if (returnType.isEquivalentTo(classOf[akka.persistence.typed.scaladsl.ReplyEffect[_, _]])) {
genericReturnType match {
caseparameterizedType: ParameterizedType=>valArray(eventType, stateType) = parameterizedType.getActualTypeArguments
checkType(eventType, "event", "when saving to the journal")
checkType(stateType, "persistent state", "when doing a snapshot")
case _ =>
}
} elseif (returnType.isEquivalentTo(classOf[akka.projection.eventsourced.EventEnvelope[_]])) {
genericReturnType match {
caseparameterizedType: ParameterizedType=>valArray(eventType) = parameterizedType.getActualTypeArguments
checkType(eventType, "event", "when saving to the journal")
case _ =>
}
} elseif (returnType.isEquivalentTo(classOf[akka.actor.typed.Behavior[_]])) {
genericReturnType match {
caseparameterizedType: ParameterizedType=>valArray(messageType) = parameterizedType.getActualTypeArguments
checkType(messageType, "message", "when sending a message outside of the current JVM")
case _ =>
}
}
}
}
})
.check(importedProductionClasses)
}
}
// The below tests (roughly) check that Jackson serialization of classes marked as CborSerializable// will actually succeed in the runtime.// TODO (GMV3-763): migrate serialization checks from ArchUnit to compile-time verification (Borer-based?)// The below tests do NOT ensure that the the classes used as messages/events/state in Akka// are always marked as CborSerializable."Classes that implement CborSerializable" should {
"never contain a non-serializable field" in {
// These have been semi-manually verified using SerializationTestKit.valsafeGenericNonCborSerializableTypes:Seq[Class[_]] =Seq(
classOf[akka.actor.typed.ActorRef[_]],
classOf[akka.stream.SourceRef[_]],
classOf[cats.data.NonEmptyList[_]],
classOf[scala.Option[_]],
classOf[scala.collection.immutable.List[_]],
classOf[scala.collection.immutable.Map[_, _]],
classOf[scala.collection.immutable.Seq[_]],
classOf[scala.collection.immutable.Set[_]],
classOf[scala.collection.immutable.SortedSet[_]],
classOf[scala.collection.Seq[_]])
valmapGenericTypes:Seq[Class[_]] =Seq(classOf[scala.collection.Map[_, _]])
valsortedGenericTypes:Seq[Class[_]] =Seq(
// Explicit upcast required due to a glitch in Scala 2.12// (apparently has trouble finding a common supertype for existential types?// although that's not a problem in safeGenericNonCborSerializableTypes above... weird)classOf[scala.collection.immutable.SortedMap[_, _]]:Class[_],
classOf[scala.collection.immutable.SortedSet[_]]:Class[_])
// These have been semi-manually verified using SerializationTestKit.valsafeNonCborSerializableTypes:Seq[Class[_]] =Seq(
java.lang.Boolean.TYPE,
java.lang.Byte.TYPE,
java.lang.Double.TYPE,
java.lang.Integer.TYPE,
java.lang.Long.TYPE,
classOf[java.lang.String],
classOf[java.lang.Throwable],
classOf[java.time.Instant],
classOf[java.time.OffsetDateTime],
classOf[scala.concurrent.duration.FiniteDuration],
classOf[scala.math.BigDecimal])
classes.that
.areAssignableTo(classOf[myproject.CborSerializable])
.should(newArchCondition[JavaClass]("only contain serializable fields") {
privatedefcheckParametrizedType(
parameterizedType: ParameterizedType,
field: JavaField,
events: ConditionEvents):Unit= {
valfieldType= field.getRawType
valfieldCaption=s"Field ${field.getName} in class ${field.getOwner.getFullName}"
parameterizedType.getActualTypeArguments.foreach {
casetypeArgumentAsParametrizedType: ParameterizedType=>
typeArgumentAsParametrizedType.getRawType match {
caserawType: Class[_] ifclassOf[CborSerializable].isAssignableFrom(rawType) =>
checkParametrizedType(typeArgumentAsParametrizedType, field, events)
caserawType: Class[_] if safeGenericNonCborSerializableTypes.contains(rawType) =>
checkParametrizedType(typeArgumentAsParametrizedType, field, events)
case rawType =>valmessage= fieldCaption +s"is of a generic type $parameterizedType, "+s"whose one of type arguments is $rawType (itself a generic type), "+s"which does NOT extend CborSerializable and has NOT been verified by the team to be serializable yet "+s"(tip: mark it as CborSerializable or use Akka's SerializationTestKit#verify(...) "+s"on a case class that has a field of ${rawType.getTypeName} type)"
events.add(SimpleConditionEvent.violated(fieldType, message))
}
casetypeArgumentAsClass: Class[_] if typeArgumentAsClass ==classOf[java.lang.Object] =>// For some reason, we observed that when type parameter is Boolean in Scala code,// then the generic type recorded in classfile is java.lang.Object rather than java.lang.Boolean.// java.lang.Object isn't likely to appear for any other type (unless someone uses Any or AnyRef as a type param),// so we're giving it a free pass.casetypeArgumentAsClass: Class[_] if safeNonCborSerializableTypes.contains(typeArgumentAsClass) =>// OK, expectedcasetypeArgumentAsClass: Class[_] ifclassOf[CborSerializable].isAssignableFrom(typeArgumentAsClass) =>// OK, expectedcase typeArgument =>valmessage= fieldCaption +s"is of a generic type $parameterizedType, "+s"whose one of type arguments is $typeArgument, "+s"which does NOT extend CborSerializable and has NOT been verified by the team to be serializable yet "+s"(tip: mark it as CborSerializable or use Akka's SerializationTestKit#verify(...) "+s"on a case class that has a field of ${typeArgument.getTypeName} type)"
events.add(SimpleConditionEvent.violated(fieldType, message))
}
}
overridedefcheck(clazz: JavaClass, events: ConditionEvents):Unit= {
clazz.getFields.forEach { field =>valfieldType= field.getRawType
valfieldCaption=s"Field ${field.getName} in class ${field.getOwner.getFullName}"if (mapGenericTypes.exists(fieldType.isEquivalentTo)) {
valfieldGenericType= field.reflect.getGenericType
valArray(keyType, _) = fieldGenericType.asInstanceOf[ParameterizedType].getActualTypeArguments
if (keyType !=classOf[String]) {
valctor= clazz.getConstructors.asScala.head.reflect
valctorsParamForField= ctor.getParameters.find(_.getName == field.getName).get
valannotationsOfCtorsParamForField= ctorsParamForField.getAnnotations.toSeq
valjsonSerializeOpt=
annotationsOfCtorsParamForField.find(_.annotationType ==classOf[JsonSerialize])
valjsonDeserializeOpt=
annotationsOfCtorsParamForField.find(_.annotationType ==classOf[JsonDeserialize])
(jsonSerializeOpt, jsonDeserializeOpt) match {
// "None" corresponds to com.fasterxml.jackson.databind.JsonSerializer.None, the default class for `keyUsing`// This can't be easily done using `classOf`// since accessing Java's static nested classes is cumbersome from Scalacase (Some(jsonSerialize: JsonSerialize), Some(jsonDeserialize: JsonDeserialize))
if jsonSerialize.keyUsing.getSimpleName !="None"&&
jsonDeserialize.keyUsing.getSimpleName !="None"=>// OK, expectedcase _ =>valmessage= fieldCaption +s"is of a map type $fieldGenericType, "+s"whose key type argument is $keyType, "+s"which is NOT annotated with both @JsonSerialize(keyUsing = ...) and @JsonDeserialize(keyUsing = ...)"
events.add(SimpleConditionEvent.violated(fieldType, message))
}
}
}
if (sortedGenericTypes.exists(fieldType.isEquivalentTo)) {
valfieldGenericType= field.reflect.getGenericType
valArray(keyType, _*) = fieldGenericType.asInstanceOf[ParameterizedType].getActualTypeArguments
defisComparable(clazz: Class[_]):Boolean=classOf[java.lang.Comparable[_]].isAssignableFrom(clazz)
keyType match {
casekeyClazz: Class[_] if isComparable(keyClazz) =>// OK, expectedcasekeyClazz: ParameterizedTypeif isComparable(keyClazz.getRawType.asInstanceOf[Class[_]]) =>// OK, expectedcase _ =>valmessage= fieldCaption +s"is of a sorted type $fieldGenericType, "+s"whose key type argument is $keyType, "+s"which does NOT extend java.lang.Comparable; this is bound to fail on deserialization in the runtime"
events.add(SimpleConditionEvent.violated(fieldType, message))
}
}
if (safeGenericNonCborSerializableTypes.exists(fieldType.isEquivalentTo)) {
checkParametrizedType(field.reflect.getGenericType.asInstanceOf[ParameterizedType], field, events)
} elseif (!fieldType.isAssignableTo(classOf[CborSerializable])) {
if (!safeNonCborSerializableTypes.exists(fieldType.isEquivalentTo)) {
valmessage= fieldCaption +s"is of a non-${classOf[CborSerializable].getSimpleName} type ${fieldType.getFullName}, "+s"which does NOT extend CborSerializable and has NOT been verified by the team to be serializable yet "+s"(tip: mark it as CborSerializable or use Akka's SerializationTestKit#verify(...)"+s" on a case class that has a field of ${fieldType.getFullName} type)"
events.add(SimpleConditionEvent.violated(fieldType, message))
}
} elseif (!fieldType.isConcrete &&!fieldType.hasExplicitJacksonAnnotations) {
valinterfaces= clazz.getAllInterfaces.asScala
if (!interfaces.exists(_.hasExplicitJacksonAnnotations)) {
valmessage= fieldCaption +s"is of a non-concrete type ${fieldType.getFullName}, "+"which is NOT annotated with either (@JsonSerialize + @JsonDeserialize) OR @JsonTypeInfo,"+s"and neither is any of its implemented interfaces (${interfaces.map(_.getFullName).mkString(", ")})"
events.add(SimpleConditionEvent.violated(fieldType, message))
}
}
}
}
})
.check(importedProductionClasses)
}
"not be of unannotated abstract type" in {
classes.that
.areAssignableTo(classOf[myproject.CborSerializable])
.and
.areNotAssignableFrom(classOf[myproject.CborSerializable])
.should(newArchCondition[JavaClass]("not be of unannotated abstract type") {
overridedefcheck(clazz: JavaClass, events: ConditionEvents):Unit= {
if (!clazz.isConcrete &&!clazz.hasExplicitJacksonAnnotations) {
valinterfaces= clazz.getAllInterfaces.asScala
if (!interfaces.exists(_.hasExplicitJacksonAnnotations)) {
valmessage=s"${clazz.getFullName} of a non-concrete type ${clazz.getFullName}, "+"which is NOT annotated with either (@JsonSerialize + @JsonDeserialize) OR @JsonTypeInfo, "+s"and neither is any of its implemented interfaces (${interfaces.map(_.getFullName).mkString(", ")})"
events.add(SimpleConditionEvent.violated(clazz, message))
}
}
}
})
.check(importedProductionClasses)
}
"not be Scala objects" in {
noClasses.that
.areAssignableTo(classOf[myproject.CborSerializable])
.and
// Enums get a free pass since they have a dedicated deserializer.
.areNotAssignableTo(classOf[enumeratum.EnumEntry])
.and
// This one is scheduled for removal in favor of myproject.core.currency.MoneyAmount anyway.
.areNotAssignableTo(myproject.core.currency.DefaultCurrency.getClass)
.should(newArchCondition[JavaClass]("be Scala objects") {
overridedefcheck(clazz: JavaClass, events: ConditionEvents):Unit= {
if (clazz.reflect().isScalaObject) {
valmessage=s"${clazz.getFullName} is a Scala object"
events.add(SimpleConditionEvent.satisfied(clazz, message))
}
}
})
.because("Jackson deserializer is NOT aware that Scala objects are supposed to be singletons, "+"and creates new instances of the object's class instead, which makes pattern matching on objects completely unreliable; "+"use a nullary case class instead of an object")
.check(importedProductionClasses)
}
}
"Classes that implement both CborSerializable and EnumEntry" should {
"be serialized with EnumEntry(De)Serializer" in {
classes.that.areInterfaces.and
.areAssignableTo(classOf[enumeratum.EnumEntry])
.and
.areAssignableTo(classOf[myproject.CborSerializable])
.should
.beAnnotatedWith(newDescribedPredicate[JavaAnnotation[_]](
s"JsonSerialize(classOf[_ <: ${classOf[EnumEntrySerializer[_]].getSimpleName}])") {
overridedefapply(input: JavaAnnotation[_]):Boolean=if (input.getRawType.isAssignableTo(classOf[JsonSerialize])) {
input.getProperties.getOrDefault("using", null) match { // scalastyle:ignore nullcaseu: JavaClassif u.isAssignableTo(classOf[EnumEntrySerializer[_]]) =>truecase _ =>false
}
} else {
false
}
})
.andShould
.beAnnotatedWith(newDescribedPredicate[JavaAnnotation[_]](
s"JsonDeserialize(classOf[_ <: ${classOf[EnumEntryDeserializer[_]].getSimpleName}])") {
overridedefapply(input: JavaAnnotation[_]):Boolean=if (input.getRawType.isAssignableTo(classOf[JsonDeserialize])) {
input.getProperties.getOrDefault("using", null) match { // scalastyle:ignore nullcaseu: JavaClassif u.isAssignableTo(classOf[EnumEntryDeserializer[_]]) =>truecase _ =>false
}
} else {
false
}
})
.because("this will fail in the runtime otherwise")
.check(importedProductionClasses)
}
}
}
The text was updated successfully, but these errors were encountered:
I suggest toying with akka.actor.testkit.typed.scaladsl.SerializationTestKit to reproduce some of the runtime errors one can encounter with Jackson.
The simplest repro would be sth like:
traitFoocaseclassBar(foo: Foo)
valtestKit:ActorTestKit=ActorTestKit()
valsystem:ActorSystem[Nothing] = testKit.system
valserializationTestKit=newSerializationTestKit(system)
serializationTestKit.verify(Bar(newFoo {})) // will likely fail since Foo is an abstract type without `@JsonSubType` annotation
The purpose of this task is mostly for you to realize how poor the Jackson serialization is in Akka, and what Jackson's problems this project is supposed to alleviate.
That's a test suite from my current project that strives to capture all the problems discovered with Jackson serializer so far.
The text was updated successfully, but these errors were encountered: