CORDA-1905: Extend JSON deserialisation to handle Amount<T> for any T. (#3790)

* Extend JSON deserialisation to handle Amount<T> for any T.
* Rewrite message for @Deprecated.
This commit is contained in:
Chris Rankin 2018-08-17 17:20:26 +01:00 committed by GitHub
parent 4634283665
commit 494661cc0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 175 additions and 9 deletions

View File

@ -413,10 +413,10 @@ object JacksonSupport {
}
}
@Deprecated("This is an internal class, do not use")
@Deprecated("Do not use - Replaced by Corda's internal AmountDeserializer and TokenDeserializer classes")
object AmountDeserializer : JsonDeserializer<Amount<*>>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): Amount<*> {
return if (parser.currentToken == JsonToken.VALUE_STRING) {
return if (parser.currentToken() == JsonToken.VALUE_STRING) {
Amount.parseCurrency(parser.text)
} else {
val wrapper = parser.readValueAs<CurrencyAmountWrapper>()

View File

@ -2,10 +2,9 @@
package net.corda.client.jackson.internal
import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.annotation.JsonCreator.Mode.*
import com.fasterxml.jackson.annotation.JsonInclude.Include
import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.annotation.JsonValue
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParseException
import com.fasterxml.jackson.core.JsonParser
@ -13,6 +12,9 @@ import com.fasterxml.jackson.core.JsonToken
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.databind.node.IntNode
import com.fasterxml.jackson.databind.node.ObjectNode
@ -40,15 +42,18 @@ import net.corda.serialization.internal.amqp.SerializerFactory
import net.corda.serialization.internal.amqp.constructorForDeserialization
import net.corda.serialization.internal.amqp.hasCordaSerializable
import net.corda.serialization.internal.amqp.propertiesForSerialization
import java.math.BigDecimal
import java.security.PublicKey
import java.security.cert.CertPath
import java.time.Instant
import java.util.*
class CordaModule : SimpleModule("corda-core") {
override fun setupModule(context: SetupContext) {
super.setupModule(context)
context.addBeanSerializerModifier(CordaSerializableBeanSerializerModifier())
context.addBeanDeserializerModifier(AmountBeanDeserializerModifier())
context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java)
context.setMixInAnnotations(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java)
@ -407,9 +412,78 @@ private interface SecureHashSHA256Mixin
@JsonDeserialize(using = JacksonSupport.PublicKeyDeserializer::class)
private interface PublicKeyMixin
@Suppress("unused_parameter")
@ToStringSerialize
@JsonDeserialize(using = JacksonSupport.AmountDeserializer::class)
private interface AmountMixin
private abstract class AmountMixin @JsonCreator(mode = DISABLED) constructor(
quantity: Long,
displayTokenSize: BigDecimal,
token: Any
) {
/**
* This mirrors the [Amount] constructor that we want Jackson to use, and
* requires that we also tell Jackson NOT to use [Amount]'s primary constructor.
*/
@JsonCreator constructor(
@JsonProperty("quantity")
quantity: Long,
@JsonDeserialize(using = TokenDeserializer::class)
@JsonProperty("token")
token: Any
) : this(quantity, Amount.getDisplayTokenSize(token), token)
}
/**
* Implements polymorphic deserialization for [Amount.token]. Kotlin must
* be able to determine the concrete [Amount] type at runtime, or it will
* fall back to using [Currency].
*/
private class TokenDeserializer(private val tokenType: Class<*>) : JsonDeserializer<Any>(), ContextualDeserializer {
@Suppress("unused")
constructor() : this(Currency::class.java)
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): Any = parser.readValueAs(tokenType)
override fun createContextual(ctxt: DeserializationContext, property: BeanProperty?): TokenDeserializer {
if (property == null) return this
return TokenDeserializer(property.type.rawClass.let { type ->
if (type == Any::class.java) Currency::class.java else type
})
}
}
/**
* Intercepts bean-based deserialization for the generic [Amount] type.
*/
private class AmountBeanDeserializerModifier : BeanDeserializerModifier() {
override fun modifyDeserializer(config: DeserializationConfig, description: BeanDescription, deserializer: JsonDeserializer<*>): JsonDeserializer<*> {
val modified = super.modifyDeserializer(config, description, deserializer)
return if (Amount::class.java.isAssignableFrom(description.beanClass)) {
AmountDeserializer(modified)
} else {
modified
}
}
}
private class AmountDeserializer(delegate: JsonDeserializer<*>) : DelegatingDeserializer(delegate) {
override fun newDelegatingInstance(newDelegatee: JsonDeserializer<*>) = AmountDeserializer(newDelegatee)
override fun deserialize(parser: JsonParser, context: DeserializationContext?): Any {
return if (parser.currentToken() == JsonToken.VALUE_STRING) {
/*
* This is obviously specific to Amount<Currency>, and is here to
* preserve the original deserializing behaviour for this case.
*/
Amount.parseCurrency(parser.text)
} else {
/*
* Otherwise continue deserializing our Bean as usual.
*/
_delegatee.deserialize(parser, context)
}
}
}
@JsonDeserialize(using = JacksonSupport.OpaqueBytesDeserializer::class)
private interface ByteSequenceMixin {

View File

@ -0,0 +1,88 @@
package net.corda.client.jackson
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.TextNode
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import net.corda.client.jackson.internal.CordaModule
import net.corda.core.contracts.Amount
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
import java.util.*
class AmountTest {
private companion object {
private val CO2 = CarbonCredit("CO2")
private val jsonMapper: ObjectMapper = ObjectMapper().registerModule(CordaModule())
private val yamlMapper: ObjectMapper = ObjectMapper(YAMLFactory()).registerModule(CordaModule())
}
@Test
fun `Amount(Currency) JSON deserialization`() {
val str = """{ "quantity": 100, "token": "USD" }"""
val amount = jsonMapper.readValue<Amount<Currency>>(str, object : TypeReference<Amount<Currency>>() {})
assertThat(amount.quantity).isEqualTo(100)
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
}
@Test
fun `Amount(Currency) YAML deserialization`() {
val str = """{ quantity: 100, token: USD }"""
val amount = yamlMapper.readValue<Amount<Currency>>(str, object : TypeReference<Amount<Currency>>() {})
assertThat(amount.quantity).isEqualTo(100)
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
}
@Test
fun `Amount(CarbonCredit) JSON deserialization`() {
val str = """{ "quantity": 200, "token": { "type": "CO2" } }"""
val amount = jsonMapper.readValue<Amount<CarbonCredit>>(str, object : TypeReference<Amount<CarbonCredit>>() {})
assertThat(amount.quantity).isEqualTo(200)
assertThat(amount.token).isEqualTo(CO2)
}
@Test
fun `Amount(CarbonCredit) YAML deserialization`() {
val str = """{ quantity: 250, token: { type: CO2 } }"""
val amount = yamlMapper.readValue<Amount<CarbonCredit>>(str, object : TypeReference<Amount<CarbonCredit>>() {})
assertThat(amount.quantity).isEqualTo(250)
assertThat(amount.token).isEqualTo(CO2)
}
@Test
fun `Amount(Unknown) JSON deserialization`() {
val str = """{ "quantity": 100, "token": "USD" }"""
val amount = jsonMapper.readValue<Amount<*>>(str, object : TypeReference<Amount<*>>() {})
assertThat(amount.quantity).isEqualTo(100)
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
}
@Test
fun `Amount(Unknown) YAML deserialization`() {
val str = """{ quantity: 100, token: USD }"""
val amount = yamlMapper.readValue<Amount<*>>(str, object : TypeReference<Amount<*>>() {})
assertThat(amount.quantity).isEqualTo(100)
assertThat(amount.token).isEqualTo(Currency.getInstance("USD"))
}
@Test
fun `Amount(Currency) YAML serialization`() {
assertThat(yamlMapper.valueToTree<TextNode>(Amount.parseCurrency("£25000000"))).isEqualTo(TextNode("25000000.00 GBP"))
assertThat(yamlMapper.valueToTree<TextNode>(Amount.parseCurrency("$250000"))).isEqualTo(TextNode("250000.00 USD"))
}
@Test
fun `Amount(CarbonCredit) JSON serialization`() {
assertThat(jsonMapper.writeValueAsString(Amount(123456, CO2)).trim())
.isEqualTo(""""123456 CarbonCredit(type=CO2)"""")
}
@Test
fun `Amount(CarbonCredit) YAML serialization`() {
assertThat(yamlMapper.writeValueAsString(Amount(123456, CO2)).trim())
.isEqualTo("""--- "123456 CarbonCredit(type=CO2)"""")
}
data class CarbonCredit(@JsonProperty("type") val type: String)
}

View File

@ -95,10 +95,14 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory:
@Test
fun `Amount(Currency) deserialization`() {
val old = mapOf(
"quantity" to 2500000000,
"token" to "USD"
"quantity" to 2500000000,
"token" to "USD"
)
assertThat(mapper.convertValue<Amount<Currency>>(old)).isEqualTo(Amount(2_500_000_000, USD))
}
@Test
fun `Amount(Currency) Text deserialization`() {
assertThat(mapper.convertValue<Amount<Currency>>(TextNode("$25000000"))).isEqualTo(Amount(2_500_000_000, USD))
}