From 3c4212a3d6d79d38afddd218ffc65dfbb8cbd5d5 Mon Sep 17 00:00:00 2001
From: Katelyn Baker <katelyn.baker@r3.com>
Date: Mon, 12 Feb 2018 10:07:25 +0000
Subject: [PATCH] CORDA-992 - Make the finger printer pluggable for
 serialization factory (#2479)

Facilitates easier testing
---
 docs/source/changelog.rst                     |   2 +
 .../serialization/amqp/ArraySerializer.kt     |   3 +-
 .../amqp/CollectionSerializer.kt              |   4 +-
 .../serialization/amqp/CustomSerializer.kt    |   4 +-
 .../amqp/EnumEvolutionSerializer.kt           |   3 +-
 .../serialization/amqp/EnumSerializer.kt      |   3 +-
 .../serialization/amqp/FingerPrinter.kt       | 202 ++++++++++++++++++
 .../serialization/amqp/MapSerializer.kt       |   3 +-
 .../serialization/amqp/ObjectSerializer.kt    |   3 +-
 .../internal/serialization/amqp/Schema.kt     | 173 +--------------
 .../serialization/amqp/SerializerFactory.kt   |   8 +-
 .../serialization/amqp/SingletonSerializer.kt |   3 +-
 .../serialization/amqp/ErrorMessageTests.java |   4 +-
 .../serialization/amqp/JavaGenericsTest.java  |  12 +-
 .../amqp/JavaPrivatePropertyTests.java        |  28 +--
 .../amqp/JavaSerialiseEnumTests.java          |   4 +-
 .../amqp/JavaSerializationOutputTests.java    |   7 +-
 .../amqp/ListsSerializationJavaTest.java      |   9 +-
 .../amqp/SetterConstructorTests.java          |  20 +-
 .../amqp/FingerPrinterTesting.kt              |  55 +++++
 20 files changed, 347 insertions(+), 203 deletions(-)
 create mode 100644 node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinter.kt
 create mode 100644 node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinterTesting.kt

diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst
index bccaf5c085..23a00b1ada 100644
--- a/docs/source/changelog.rst
+++ b/docs/source/changelog.rst
@@ -6,6 +6,8 @@ from the previous milestone release.
 
 UNRELEASED
 ----------
+* Make the serialisation finger-printer a pluggable entity rather than hard wiring into the factory
+
 * Removed blacklisted word checks in Corda X.500 name to allow "Server" or "Node" to be use as part of the legal name.
 
 * Separated our pre-existing Artemis broker into an RPC broker and a P2P broker.
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt
index b6b20a204e..872709a119 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ArraySerializer.kt
@@ -31,7 +31,8 @@ open class ArraySerializer(override val type: Type, factory: SerializerFactory)
                 "${type.componentType().typeName}$arrayType"
             }
 
-    override val typeDescriptor by lazy { Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}") }
+    override val typeDescriptor by lazy {
+        Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}") }
     internal val elementType: Type by lazy { type.componentType() }
     internal open val typeName by lazy { calcTypeName(type) }
 
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt
index bd19e98599..5a6ab42f8e 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CollectionSerializer.kt
@@ -17,7 +17,9 @@ import kotlin.collections.Set
  */
 class CollectionSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
     override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType))
-    override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
+    override val typeDescriptor by lazy {
+        Symbol.valueOf("$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
+    }
 
     companion object {
         // NB: Order matters in this map, the most specific classes should be listed at the end
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt
index f2ee28d01b..fd02997c49 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/CustomSerializer.kt
@@ -60,7 +60,9 @@ abstract class CustomSerializer<T : Any> : AMQPSerializer<T>, SerializerFor {
 
         override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz
         override val type: Type get() = clazz
-        override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}")
+        override val typeDescriptor by lazy {
+            Symbol.valueOf("$DESCRIPTOR_DOMAIN:${SerializerFingerPrinter().fingerprintForDescriptors(superClassSerializer.typeDescriptor.toString(), nameForType(clazz))}")
+        }
         private val typeNotation: TypeNotation = RestrictedType(
                 SerializerFactory.nameForType(clazz),
                 null,
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt
index a9bc0916b1..a6d9ebb055 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumEvolutionSerializer.kt
@@ -39,7 +39,8 @@ class EnumEvolutionSerializer(
         factory: SerializerFactory,
         private val conversions: Map<String, String>,
         private val ordinals: Map<String, Int>) : AMQPSerializer<Any> {
-    override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")!!
+    override val typeDescriptor = Symbol.valueOf(
+            "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
 
     companion object {
         private fun MutableMap<String, String>.mapInPlace(f: (String) -> String) {
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt
index 5678f094ed..c3d9fdd732 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/EnumSerializer.kt
@@ -11,8 +11,9 @@ import java.lang.reflect.Type
  */
 class EnumSerializer(declaredType: Type, declaredClass: Class<*>, factory: SerializerFactory) : AMQPSerializer<Any> {
     override val type: Type = declaredType
-    override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")!!
     private val typeNotation: TypeNotation
+    override val typeDescriptor = Symbol.valueOf(
+            "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")!!
 
     init {
         typeNotation = RestrictedType(
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinter.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinter.kt
new file mode 100644
index 0000000000..e5cc478acb
--- /dev/null
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinter.kt
@@ -0,0 +1,202 @@
+package net.corda.nodeapi.internal.serialization.amqp
+
+import com.google.common.hash.Hasher
+import com.google.common.hash.Hashing
+import net.corda.core.utilities.loggerFor
+import net.corda.core.utilities.toBase64
+import java.io.NotSerializableException
+import java.lang.reflect.*
+import java.util.*
+
+/**
+ * Should be implemented by classes which wish to provide plugable fingerprinting og types for a [SerializerFactory]
+ */
+interface FingerPrinter {
+    /**
+     * Return a unique identifier for a type, usually this will take into account the constituent elements
+     * of said type such that any modification to any sub element wll generate a different fingerprint
+     */
+    fun fingerprint(type: Type): String
+
+    /**
+     * If required, associate an instance of the fingerprinter with a specific serializer factory
+     */
+    fun setOwner(factory: SerializerFactory)
+}
+
+/**
+ * Implementation of the finger printing mechanism used by default
+ */
+class SerializerFingerPrinter : FingerPrinter {
+    private var factory: SerializerFactory? = null
+
+    private val ARRAY_HASH: String = "Array = true"
+    private val ENUM_HASH: String = "Enum = true"
+    private val ALREADY_SEEN_HASH: String = "Already seen = true"
+    private val NULLABLE_HASH: String = "Nullable = true"
+    private val NOT_NULLABLE_HASH: String = "Nullable = false"
+    private val ANY_TYPE_HASH: String = "Any type = true"
+    private val TYPE_VARIABLE_HASH: String = "Type variable = true"
+    private val WILDCARD_TYPE_HASH: String = "Wild card = true"
+
+    private val logger by lazy { loggerFor<Schema>() }
+
+    override fun setOwner(factory: SerializerFactory) {
+        this.factory = factory
+    }
+
+    /**
+     * The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation.
+     * Thus it only takes into account properties and types and only supports the same object graph subset as the overall
+     * serialization code.
+     *
+     * The idea being that even for two classes that share the same name but differ in a minor way, the fingerprint will be
+     * different.
+     */
+    override fun fingerprint(type: Type): String {
+        return fingerprintForType(
+                type, null, HashSet(), Hashing.murmur3_128().newHasher(), debugIndent = 1).hash().asBytes().toBase64()
+    }
+
+    private fun isCollectionOrMap(type: Class<*>) =
+            (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type))
+                    && !EnumSet::class.java.isAssignableFrom(type)
+
+    internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String {
+        val hasher = Hashing.murmur3_128().newHasher()
+        for (typeDescriptor in typeDescriptors) {
+            hasher.putUnencodedChars(typeDescriptor)
+        }
+        return hasher.hash().asBytes().toBase64()
+    }
+
+    private fun Hasher.fingerprintWithCustomSerializerOrElse(
+            factory: SerializerFactory,
+            clazz: Class<*>,
+            declaredType: Type,
+            block: () -> Hasher): Hasher {
+        // Need to check if a custom serializer is applicable
+        val customSerializer = factory.findCustomSerializer(clazz, declaredType)
+        return if (customSerializer != null) {
+            putUnencodedChars(customSerializer.typeDescriptor)
+        } else {
+            block()
+        }
+    }
+
+    // This method concatenates various elements of the types recursively as unencoded strings into the hasher,
+    // effectively creating a unique string for a type which we then hash in the calling function above.
+    private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet<Type>,
+                                   hasher: Hasher, debugIndent: Int = 1): Hasher {
+        // We don't include Example<?> and Example<T> where type is ? or T in this otherwise we
+        // generate different fingerprints for class Outer<T>(val a: Inner<T>) when serialising
+        // and deserializing (assuming deserialization is occurring in a factory that didn't
+        // serialise the object in the  first place (and thus the cache lookup fails). This is also
+        // true of Any, where we need  Example<A, B> and Example<?, ?> to have the same fingerprint
+        return if ((type in alreadySeen)
+                && (type !is SerializerFactory.AnyType)
+                && (type !is TypeVariable<*>)
+                && (type !is WildcardType)) {
+            hasher.putUnencodedChars(ALREADY_SEEN_HASH)
+        } else {
+            alreadySeen += type
+            try {
+                when (type) {
+                    is ParameterizedType -> {
+                        // Hash the rawType + params
+                        val clazz = type.rawType as Class<*>
+
+                        val startingHash = if (isCollectionOrMap(clazz)) {
+                            hasher.putUnencodedChars(clazz.name)
+                        } else {
+                            hasher.fingerprintWithCustomSerializerOrElse(factory!!, clazz, type) {
+                                fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1)
+                            }
+                        }
+
+                        // ... and concatenate the type data for each parameter type.
+                        type.actualTypeArguments.fold(startingHash) { orig, paramType ->
+                            fingerprintForType(paramType, type, alreadySeen, orig, debugIndent + 1)
+                        }
+                    }
+                // Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing
+                // the signature of the fingerprinted object. This, however, doesn't work as it breaks bi-
+                // directional fingerprints. That is, fingerprinting a concrete instance of a generic
+                // type (Example<Int>), creates a different fingerprint from the generic type itself (Example<T>)
+                //
+                // On serialization Example<Int> is treated as Example<T>, a TypeVariable
+                // On deserialisation it is seen as Example<?>, A WildcardType *and* a TypeVariable
+                //      Note: AnyType is a special case of WildcardType used in other parts of the
+                //            serializer so both cases need to be dealt with here
+                //
+                // If we treat these types as fundamentally different and alter the fingerprint we will
+                // end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter.
+                    is SerializerFactory.AnyType,
+                    is WildcardType,
+                    is TypeVariable<*> -> {
+                        hasher.putUnencodedChars("?").putUnencodedChars(ANY_TYPE_HASH)
+                    }
+                    is Class<*> -> {
+                        if (type.isArray) {
+                            fingerprintForType(type.componentType, contextType, alreadySeen, hasher, debugIndent + 1)
+                                    .putUnencodedChars(ARRAY_HASH)
+                        } else if (SerializerFactory.isPrimitive(type)) {
+                            hasher.putUnencodedChars(type.name)
+                        } else if (isCollectionOrMap(type)) {
+                            hasher.putUnencodedChars(type.name)
+                        } else if (type.isEnum) {
+                            // ensures any change to the enum (adding constants) will trigger the need for evolution
+                            hasher.apply {
+                                type.enumConstants.forEach {
+                                    putUnencodedChars(it.toString())
+                                }
+                            }.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH)
+                        } else {
+                            hasher.fingerprintWithCustomSerializerOrElse(factory!!, type, type) {
+                                if (type.kotlin.objectInstance != null) {
+                                    // TODO: name collision is too likely for kotlin objects, we need to introduce some reference
+                                    // to the CorDapp but maybe reference to the JAR in the short term.
+                                    hasher.putUnencodedChars(type.name)
+                                } else {
+                                    fingerprintForObject(type, type, alreadySeen, hasher, factory!!, debugIndent + 1)
+                                }
+                            }
+                        }
+                    }
+                // Hash the element type + some array hash
+                    is GenericArrayType -> {
+                        fingerprintForType(type.genericComponentType, contextType, alreadySeen,
+                                hasher, debugIndent + 1).putUnencodedChars(ARRAY_HASH)
+                    }
+                    else -> throw NotSerializableException("Don't know how to hash")
+                }
+            } catch (e: NotSerializableException) {
+                val msg = "${e.message} -> $type"
+                logger.error(msg, e)
+                throw NotSerializableException(msg)
+            }
+        }
+    }
+
+    private fun fingerprintForObject(
+            type: Type,
+            contextType: Type?,
+            alreadySeen: MutableSet<Type>,
+            hasher: Hasher,
+            factory: SerializerFactory,
+            debugIndent: Int = 0): Hasher {
+        // Hash the class + properties + interfaces
+        val name = type.asClass()?.name
+                ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type")
+
+        propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory)
+                .serializationOrder
+                .fold(hasher.putUnencodedChars(name)) { orig, prop ->
+                    fingerprintForType(prop.getter.resolvedType, type, alreadySeen, orig, debugIndent + 1)
+                            .putUnencodedChars(prop.getter.name)
+                            .putUnencodedChars(if (prop.getter.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
+                }
+        interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, debugIndent + 1) }
+        return hasher
+    }
+}
\ No newline at end of file
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt
index 5472074d62..138326dde6 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/MapSerializer.kt
@@ -19,7 +19,8 @@ private typealias MapCreationFunction = (Map<*, *>) -> Map<*, *>
  */
 class MapSerializer(private val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
     override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(SerializerFactory.nameForType(declaredType))
-    override val typeDescriptor: Symbol = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
+    override val typeDescriptor: Symbol = Symbol.valueOf(
+            "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
 
     companion object {
         // NB: Order matters in this map, the most specific classes should be listed at the end
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt
index fa723fd3de..e4dddec1da 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/ObjectSerializer.kt
@@ -31,7 +31,8 @@ open class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPS
 
     private val typeName = nameForType(clazz)
 
-    override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
+    override val typeDescriptor = Symbol.valueOf(
+            "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
 
     // We restrict to only those annotated or whitelisted
     private val interfaces = interfacesForSerialization(clazz, factory)
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt
index f5edd923c9..285a1f5d54 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/Schema.kt
@@ -1,19 +1,13 @@
 package net.corda.nodeapi.internal.serialization.amqp
 
-import com.google.common.hash.Hasher
-import com.google.common.hash.Hashing
 import net.corda.core.internal.uncheckedCast
 import net.corda.nodeapi.internal.serialization.CordaSerializationMagic
-import net.corda.core.utilities.loggerFor
-import net.corda.core.utilities.toBase64
 import org.apache.qpid.proton.amqp.DescribedType
 import org.apache.qpid.proton.amqp.Symbol
 import org.apache.qpid.proton.amqp.UnsignedInteger
 import org.apache.qpid.proton.amqp.UnsignedLong
 import org.apache.qpid.proton.codec.DescribedTypeConstructor
 import java.io.NotSerializableException
-import java.lang.reflect.*
-import java.util.*
 import net.corda.nodeapi.internal.serialization.carpenter.Field as CarpenterField
 import net.corda.nodeapi.internal.serialization.carpenter.Schema as CarpenterSchema
 
@@ -92,7 +86,14 @@ data class Descriptor(val name: Symbol?, val code: UnsignedLong? = null) : Descr
     }
 }
 
-data class Field(val name: String, val type: String, val requires: List<String>, val default: String?, val label: String?, val mandatory: Boolean, val multiple: Boolean) : DescribedType {
+data class Field(
+        val name: String,
+        val type: String,
+        val requires: List<String>,
+        val default: String?,
+        val label: String?,
+        val mandatory: Boolean,
+        val multiple: Boolean) : DescribedType {
     companion object : DescribedTypeConstructor<Field> {
         val DESCRIPTOR = AMQPDescriptorRegistry.FIELD.amqpDescriptor
 
@@ -302,162 +303,4 @@ data class ReferencedObject(private val refCounter: Int) : DescribedType {
     override fun toString(): String = "<refObject refCounter=$refCounter/>"
 }
 
-private val ARRAY_HASH: String = "Array = true"
-private val ENUM_HASH: String = "Enum = true"
-private val ALREADY_SEEN_HASH: String = "Already seen = true"
-private val NULLABLE_HASH: String = "Nullable = true"
-private val NOT_NULLABLE_HASH: String = "Nullable = false"
-private val ANY_TYPE_HASH: String = "Any type = true"
-private val TYPE_VARIABLE_HASH: String = "Type variable = true"
-private val WILDCARD_TYPE_HASH: String = "Wild card = true"
 
-private val logger by lazy { loggerFor<Schema>() }
-
-/**
- * The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation.
- * Thus it only takes into account properties and types and only supports the same object graph subset as the overall
- * serialization code.
- *
- * The idea being that even for two classes that share the same name but differ in a minor way, the fingerprint will be
- * different.
- */
-// TODO: write tests
-internal fun fingerprintForType(type: Type, factory: SerializerFactory): String {
-    return fingerprintForType(type, null, HashSet(), Hashing.murmur3_128().newHasher(), factory).hash().asBytes().toBase64()
-}
-
-internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String {
-    val hasher = Hashing.murmur3_128().newHasher()
-    for (typeDescriptor in typeDescriptors) {
-        hasher.putUnencodedChars(typeDescriptor)
-    }
-    return hasher.hash().asBytes().toBase64()
-}
-
-private fun Hasher.fingerprintWithCustomSerializerOrElse(factory: SerializerFactory, clazz: Class<*>, declaredType: Type, block: () -> Hasher): Hasher {
-    // Need to check if a custom serializer is applicable
-    val customSerializer = factory.findCustomSerializer(clazz, declaredType)
-    return if (customSerializer != null) {
-        putUnencodedChars(customSerializer.typeDescriptor)
-    } else {
-        block()
-    }
-}
-
-// This method concatenates various elements of the types recursively as unencoded strings into the hasher, effectively
-// creating a unique string for a type which we then hash in the calling function above.
-private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet<Type>,
-                               hasher: Hasher, factory: SerializerFactory, debugIndent: Int = 1): Hasher {
-    // We don't include Example<?> and Example<T> where type is ? or T in this otherwise we
-    // generate different fingerprints for class Outer<T>(val a: Inner<T>) when serialising
-    // and deserializing (assuming deserialization is occurring in a factory that didn't
-    // serialise the object in the  first place (and thus the cache lookup fails). This is also
-    // true of Any, where we need  Example<A, B> and Example<?, ?> to have the same fingerprint
-    return if ((type in alreadySeen)
-            && (type !is SerializerFactory.AnyType)
-            && (type !is TypeVariable<*>)
-            && (type !is WildcardType)) {
-        hasher.putUnencodedChars(ALREADY_SEEN_HASH)
-    } else {
-        alreadySeen += type
-        try {
-            when (type) {
-                is ParameterizedType -> {
-                    // Hash the rawType + params
-                    val clazz = type.rawType as Class<*>
-
-                    val startingHash = if (isCollectionOrMap(clazz)) {
-                        hasher.putUnencodedChars(clazz.name)
-                    } else {
-                        hasher.fingerprintWithCustomSerializerOrElse(factory, clazz, type) {
-                            fingerprintForObject(type, type, alreadySeen, hasher, factory, debugIndent+1)
-                        }
-                    }
-
-                    // ... and concatenate the type data for each parameter type.
-                    type.actualTypeArguments.fold(startingHash) { orig, paramType ->
-                        fingerprintForType(paramType, type, alreadySeen, orig, factory, debugIndent+1)
-                    }
-                }
-            // Previously, we drew a distinction between TypeVariable, WildcardType, and AnyType, changing
-            // the signature of the fingerprinted object. This, however, doesn't work as it breaks bi-
-            // directional fingerprints. That is, fingerprinting a concrete instance of a generic
-            // type (Example<Int>), creates a different fingerprint from the generic type itself (Example<T>)
-            //
-            // On serialization Example<Int> is treated as Example<T>, a TypeVariable
-            // On deserialisation it is seen as Example<?>, A WildcardType *and* a TypeVariable
-            //      Note: AnyType is a special case of WildcardType used in other parts of the
-            //            serializer so both cases need to be dealt with here
-            //
-            // If we treat these types as fundamentally different and alter the fingerprint we will
-            // end up breaking into the evolver when we shouldn't or, worse, evoking the carpenter.
-                is SerializerFactory.AnyType,
-                is WildcardType,
-                is TypeVariable<*> -> {
-                    hasher.putUnencodedChars("?").putUnencodedChars(ANY_TYPE_HASH)
-                }
-                is Class<*> -> {
-                    if (type.isArray) {
-                        fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory, debugIndent+1)
-                                .putUnencodedChars(ARRAY_HASH)
-                    } else if (SerializerFactory.isPrimitive(type)) {
-                        hasher.putUnencodedChars(type.name)
-                    } else if (isCollectionOrMap(type)) {
-                        hasher.putUnencodedChars(type.name)
-                    } else if (type.isEnum) {
-                        // ensures any change to the enum (adding constants) will trigger the need for evolution
-                        hasher.apply {
-                            type.enumConstants.forEach {
-                                putUnencodedChars(it.toString())
-                            }
-                        }.putUnencodedChars(type.name).putUnencodedChars(ENUM_HASH)
-                    } else {
-                        hasher.fingerprintWithCustomSerializerOrElse(factory, type, type) {
-                            if (type.kotlin.objectInstance != null) {
-                                // TODO: name collision is too likely for kotlin objects, we need to introduce some reference
-                                // to the CorDapp but maybe reference to the JAR in the short term.
-                                hasher.putUnencodedChars(type.name)
-                            } else {
-                                fingerprintForObject(type, type, alreadySeen, hasher, factory, debugIndent+1)
-                            }
-                        }
-                    }
-                }
-            // Hash the element type + some array hash
-                is GenericArrayType -> {
-                    fingerprintForType(type.genericComponentType, contextType, alreadySeen,
-                            hasher, factory, debugIndent+1).putUnencodedChars(ARRAY_HASH)
-                }
-                else -> throw NotSerializableException("Don't know how to hash")
-            }
-        } catch (e: NotSerializableException) {
-            val msg = "${e.message} -> $type"
-            logger.error(msg, e)
-            throw NotSerializableException(msg)
-        }
-    }
-}
-
-private fun isCollectionOrMap(type: Class<*>) = (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) &&
-        !EnumSet::class.java.isAssignableFrom(type)
-
-private fun fingerprintForObject(
-        type: Type,
-        contextType: Type?,
-        alreadySeen: MutableSet<Type>,
-        hasher: Hasher,
-        factory: SerializerFactory,
-        debugIndent: Int = 0): Hasher {
-    // Hash the class + properties + interfaces
-    val name = type.asClass()?.name ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type")
-
-    propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory)
-            .serializationOrder
-            .fold(hasher.putUnencodedChars(name)) { orig, prop ->
-                fingerprintForType(prop.getter.resolvedType, type, alreadySeen, orig, factory, debugIndent+1)
-                        .putUnencodedChars(prop.getter.name)
-                        .putUnencodedChars(if (prop.getter.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
-            }
-    interfacesForSerialization(type, factory).map { fingerprintForType(it, type, alreadySeen, hasher, factory, debugIndent+1) }
-    return hasher
-}
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt
index 7bd8178a6d..011b04dbf7 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SerializerFactory.kt
@@ -40,7 +40,13 @@ data class FactorySchemaAndDescriptor(val schemas: SerializationSchemas, val typ
 open class SerializerFactory(
         val whitelist: ClassWhitelist,
         cl: ClassLoader,
-        private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter()) {
+        private val evolutionSerializerGetter: EvolutionSerializerGetterBase = EvolutionSerializerGetter(),
+        val fingerPrinter: FingerPrinter = SerializerFingerPrinter()) {
+
+    init {
+        fingerPrinter.setOwner(this)
+    }
+
     private val serializersByType = ConcurrentHashMap<Type, AMQPSerializer<Any>>()
     private val serializersByDescriptor = ConcurrentHashMap<Any, AMQPSerializer<Any>>()
     private val customSerializers = CopyOnWriteArrayList<SerializerFor>()
diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt
index 0c70a18935..b54bb94393 100644
--- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt
+++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/serialization/amqp/SingletonSerializer.kt
@@ -10,7 +10,8 @@ import java.lang.reflect.Type
  * want converting back to that singleton instance on the receiving JVM.
  */
 class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: SerializerFactory) : AMQPSerializer<Any> {
-    override val typeDescriptor = Symbol.valueOf("$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}")
+    override val typeDescriptor = Symbol.valueOf(
+            "$DESCRIPTOR_DOMAIN:${factory.fingerPrinter.fingerprint(type)}")
 
     private val interfaces = interfacesForSerialization(type, factory)
 
diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ErrorMessageTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ErrorMessageTests.java
index 7cd21c4f21..11db9fc643 100644
--- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ErrorMessageTests.java
+++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ErrorMessageTests.java
@@ -32,10 +32,12 @@ public class ErrorMessageTests {
     @Test
     public void testJavaConstructorAnnotations() {
         EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
+        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
         SerializerFactory factory1 = new SerializerFactory(
                 AllWhitelist.INSTANCE,
                 ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter);
+                evolutionSerialiserGetter,
+                fingerPrinter);
 
         SerializationOutput ser = new SerializationOutput(factory1);
 
diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaGenericsTest.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaGenericsTest.java
index feb416e991..9c206ad8cd 100644
--- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaGenericsTest.java
+++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaGenericsTest.java
@@ -29,7 +29,8 @@ public class JavaGenericsTest {
         SerializerFactory factory = new SerializerFactory(
                 AllWhitelist.INSTANCE,
                 ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter());
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
 
         SerializationOutput ser = new SerializationOutput(factory);
         SerializedBytes<?> bytes = ser.serialize(a1);
@@ -44,7 +45,8 @@ public class JavaGenericsTest {
         SerializerFactory factory = new SerializerFactory(
                 AllWhitelist.INSTANCE,
                 ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter());
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
 
        return (new SerializationOutput(factory)).serialize(a);
     }
@@ -59,7 +61,8 @@ public class JavaGenericsTest {
         SerializerFactory factory = new SerializerFactory(
                 AllWhitelist.INSTANCE,
                 ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter());
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
 
         DeserializationInput des = new DeserializationInput(factory);
         return des.deserialize(bytes, A.class);
@@ -83,7 +86,8 @@ public class JavaGenericsTest {
         SerializerFactory factory = new SerializerFactory(
                 AllWhitelist.INSTANCE,
                 ClassLoader.getSystemClassLoader(),
-                new EvolutionSerializerGetter());
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
 
         SerializedBytes<?> bytes = forceWildcardSerializeFactory(new A(new Inner(29)), factory);
         Inner i = (Inner)forceWildcardDeserializeFactory(bytes, factory).getT();
diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaPrivatePropertyTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaPrivatePropertyTests.java
index b13cc2cc48..2ee89945f6 100644
--- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaPrivatePropertyTests.java
+++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaPrivatePropertyTests.java
@@ -76,9 +76,9 @@ public class JavaPrivatePropertyTests {
 
     @Test
     public void singlePrivateBooleanWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
         SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerializerGetter);
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
 
@@ -89,9 +89,10 @@ public class JavaPrivatePropertyTests {
 
     @Test
     public void singlePrivateBooleanWithNoConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
         SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerializerGetter);
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
+
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
 
@@ -103,9 +104,9 @@ public class JavaPrivatePropertyTests {
 
     @Test
     public void testCapitilsationOfIs() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
         SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerializerGetter);
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
 
@@ -119,9 +120,9 @@ public class JavaPrivatePropertyTests {
 
     @Test
     public void singlePrivateIntWithBoolean() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
         SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerializerGetter);
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
 
@@ -134,9 +135,10 @@ public class JavaPrivatePropertyTests {
 
     @Test
     public void singlePrivateWithConstructor() throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
         SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerializerGetter);
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
+
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
 
@@ -164,9 +166,11 @@ public class JavaPrivatePropertyTests {
     @Test
     public void singlePrivateWithConstructorAndGetter()
             throws NotSerializableException, NoSuchFieldException, IllegalAccessException {
-        EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
         SerializerFactory factory = new SerializerFactory(AllWhitelist.INSTANCE,
-                ClassLoader.getSystemClassLoader(), evolutionSerializerGetter);
+                ClassLoader.getSystemClassLoader(),
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
+
         SerializationOutput ser = new SerializationOutput(factory);
         DeserializationInput des = new DeserializationInput(factory);
 
diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java
index a64f9c3d9e..9be95ca396 100644
--- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java
+++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerialiseEnumTests.java
@@ -29,9 +29,9 @@ public class JavaSerialiseEnumTests {
     public void testJavaConstructorAnnotations() throws NotSerializableException {
         Bra bra = new Bra(Bras.UNDERWIRE);
 
-        EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
         SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter);
+                new EvolutionSerializerGetter(),
+                new SerializerFingerPrinter());
         SerializationOutput ser = new SerializationOutput(factory1);
         SerializedBytes<Object> bytes = ser.serialize(bra);
     }
diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java
index 4379718003..0441b2720f 100644
--- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java
+++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/JavaSerializationOutputTests.java
@@ -173,10 +173,13 @@ public class JavaSerializationOutputTests {
 
     private Object serdes(Object obj) throws NotSerializableException {
         EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
+        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
         SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter);
+                evolutionSerialiserGetter,
+                fingerPrinter);
         SerializerFactory factory2 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter);
+                evolutionSerialiserGetter,
+                fingerPrinter);
         SerializationOutput ser = new SerializationOutput(factory1);
         SerializedBytes<Object> bytes = ser.serialize(obj);
 
diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java
index c77f452b33..153681fa6a 100644
--- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java
+++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/ListsSerializationJavaTest.java
@@ -125,9 +125,12 @@ public class ListsSerializationJavaTest {
 
     // Have to have own version as Kotlin inline functions cannot be easily called from Java
     private static <T> void assertEqualAfterRoundTripSerialization(T container, Class<T> clazz) throws Exception {
-        EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
-        SerializerFactory factory1 = new SerializerFactory(AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter);
+        EvolutionSerializerGetterBase evolutionSerializerGetter = new EvolutionSerializerGetter();
+        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
+        SerializerFactory factory1 = new SerializerFactory(
+                AllWhitelist.INSTANCE, ClassLoader.getSystemClassLoader(),
+                evolutionSerializerGetter,
+                fingerPrinter);
         SerializationOutput ser = new SerializationOutput(factory1);
         SerializedBytes<Object> bytes = ser.serialize(container);
         DeserializationInput des = new DeserializationInput(factory1);
diff --git a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/SetterConstructorTests.java b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/SetterConstructorTests.java
index defd86d948..a0ab7276dd 100644
--- a/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/SetterConstructorTests.java
+++ b/node-api/src/test/java/net/corda/nodeapi/internal/serialization/amqp/SetterConstructorTests.java
@@ -110,10 +110,12 @@ public class SetterConstructorTests {
     @Test
     public void serialiseC() throws NotSerializableException {
         EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
+        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
         SerializerFactory factory1 = new SerializerFactory(
                 AllWhitelist.INSTANCE,
                 ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter);
+                evolutionSerialiserGetter,
+                fingerPrinter);
 
         SerializationOutput ser = new SerializationOutput(factory1);
 
@@ -184,10 +186,12 @@ public class SetterConstructorTests {
     @Test
     public void deserialiseC() throws NotSerializableException {
         EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
+        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
         SerializerFactory factory1 = new SerializerFactory(
                 AllWhitelist.INSTANCE,
                 ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter);
+                evolutionSerialiserGetter,
+                fingerPrinter);
 
         C cPre1 = new C();
 
@@ -251,10 +255,12 @@ public class SetterConstructorTests {
     @Test
     public void serialiseOuterAndInner() throws NotSerializableException {
         EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
+        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
         SerializerFactory factory1 = new SerializerFactory(
                 AllWhitelist.INSTANCE,
                 ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter);
+                evolutionSerialiserGetter,
+                fingerPrinter);
 
         Inner1 i1 = new Inner1("Hello");
         Inner2 i2 = new Inner2();
@@ -277,10 +283,12 @@ public class SetterConstructorTests {
     @Test
     public void typeMistmatch() throws NotSerializableException {
         EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
+        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
         SerializerFactory factory1 = new SerializerFactory(
                 AllWhitelist.INSTANCE,
                 ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter);
+                evolutionSerialiserGetter,
+                fingerPrinter);
 
         TypeMismatch tm = new TypeMismatch();
         tm.setA(10);
@@ -293,10 +301,12 @@ public class SetterConstructorTests {
     @Test
     public void typeMistmatch2() throws NotSerializableException {
         EvolutionSerializerGetterBase evolutionSerialiserGetter = new EvolutionSerializerGetter();
+        FingerPrinter fingerPrinter = new SerializerFingerPrinter();
         SerializerFactory factory1 = new SerializerFactory(
                 AllWhitelist.INSTANCE,
                 ClassLoader.getSystemClassLoader(),
-                evolutionSerialiserGetter);
+                evolutionSerialiserGetter,
+                fingerPrinter);
 
         TypeMismatch2 tm = new TypeMismatch2();
         tm.setA("10");
diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinterTesting.kt b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinterTesting.kt
new file mode 100644
index 0000000000..9b5ceb24df
--- /dev/null
+++ b/node-api/src/test/kotlin/net/corda/nodeapi/internal/serialization/amqp/FingerPrinterTesting.kt
@@ -0,0 +1,55 @@
+package net.corda.nodeapi.internal.serialization.amqp
+
+import org.junit.Test
+import java.lang.reflect.Type
+import kotlin.test.assertEquals
+import net.corda.nodeapi.internal.serialization.AllWhitelist
+
+class FingerPrinterTesting  : FingerPrinter {
+    private var index = 0
+    private val cache = mutableMapOf<Type, String>()
+
+    override fun fingerprint(type: Type): String {
+        return cache.computeIfAbsent(type) { index++.toString() }
+    }
+
+    override fun setOwner(factory: SerializerFactory) {
+        return
+    }
+
+    @Suppress("UNUSED")
+    fun changeFingerprint(type: Type) {
+        cache.computeIfAbsent(type) { "" }.apply { index++.toString() }
+    }
+}
+
+class FingerPrinterTestingTests {
+    companion object {
+        val VERBOSE = true
+    }
+    @Test
+    fun testingTest() {
+        val fpt = FingerPrinterTesting()
+        assertEquals ("0", fpt.fingerprint(Integer::class.java))
+        assertEquals ("1", fpt.fingerprint(String::class.java))
+        assertEquals ("0", fpt.fingerprint(Integer::class.java))
+        assertEquals ("1", fpt.fingerprint(String::class.java))
+    }
+
+    @Test
+    fun worksAsReplacement() {
+        data class C (val a: Int, val b: Long)
+
+        val factory = SerializerFactory(
+                AllWhitelist,
+                ClassLoader.getSystemClassLoader(),
+                EvolutionSerializerGetterTesting(),
+                FingerPrinterTesting())
+
+        val blob = TestSerializationOutput(VERBOSE, factory).serializeAndReturnSchema(C(1, 2L))
+
+        assertEquals (1, blob.schema.types.size)
+        assertEquals ("<descriptor name=\"net.corda:0\"/>", blob.schema.types[0].descriptor.toString())
+    }
+
+}
\ No newline at end of file