mirror of
https://github.com/corda/corda.git
synced 2025-01-29 15:43:55 +00:00
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:
parent
4634283665
commit
494661cc0c
@ -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>()
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
@ -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))
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user