JacksonSupport: add support for Amount<Currency> and OpaqueBytes

This commit is contained in:
Mike Hearn 2017-03-05 22:20:43 +01:00
parent 6d8ce50a41
commit 2634e1673f
5 changed files with 100 additions and 44 deletions

View File

@ -10,15 +10,18 @@ import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.core.contracts.Amount
import net.corda.core.contracts.BusinessCalendar
import net.corda.core.crypto.*
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.IdentityService
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.i2p.crypto.eddsa.EdDSAPublicKey
import java.math.BigDecimal
import java.util.*
/**
* Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for
@ -71,6 +74,14 @@ object JacksonSupport {
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
addSerializer(NodeInfo::class.java, NodeInfoSerializer)
addDeserializer(NodeInfo::class.java, NodeInfoDeserializer)
// For Amount
addSerializer(Amount::class.java, AmountSerializer)
addDeserializer(Amount::class.java, AmountDeserializer)
// For OpaqueBytes
addDeserializer(OpaqueBytes::class.java, OpaqueBytesDeserializer)
addSerializer(OpaqueBytes::class.java, OpaqueBytesSerializer)
}
}
@ -224,5 +235,43 @@ object JacksonSupport {
}
}
}
object AmountSerializer : JsonSerializer<Amount<*>>() {
override fun serialize(value: Amount<*>, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeString(value.toString())
}
}
object AmountDeserializer : JsonDeserializer<Amount<*>>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): Amount<*> {
try {
return Amount.parseCurrency(parser.text)
} catch (e: Exception) {
try {
val tree = parser.readValueAsTree<JsonNode>()
require(tree["quantity"].canConvertToLong() && tree["token"].asText().isNotBlank())
val quantity = tree["quantity"].asLong()
val token = tree["token"].asText()
// Attempt parsing as a currency token. TODO: This needs thought about how to extend to other token types.
val currency = Currency.getInstance(token)
return Amount(quantity, currency)
} catch(e2: Exception) {
throw JsonParseException(parser, "Invalid amount ${parser.text}", e2)
}
}
}
}
object OpaqueBytesDeserializer : JsonDeserializer<OpaqueBytes>() {
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): OpaqueBytes {
return OpaqueBytes(parser.text.toByteArray())
}
}
object OpaqueBytesSerializer : JsonSerializer<OpaqueBytes>() {
override fun serialize(value: OpaqueBytes, gen: JsonGenerator, serializers: SerializerProvider) {
gen.writeBinary(value.bytes)
}
}
}

View File

@ -1,12 +1,17 @@
package net.corda.jackson
import com.fasterxml.jackson.databind.SerializationFeature
import com.pholser.junit.quickcheck.From
import com.pholser.junit.quickcheck.Property
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck
import net.corda.core.contracts.Amount
import net.corda.core.contracts.USD
import net.corda.core.testing.PublicKeyGenerator
import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.junit.Test
import org.junit.runner.RunWith
import java.security.PublicKey
import java.util.*
import kotlin.test.assertEquals
@RunWith(JUnitQuickcheck::class)
@ -21,4 +26,28 @@ class JacksonSupportTest {
val parsedKey = mapper.readValue(serialized, EdDSAPublicKey::class.java)
assertEquals(publicKey, parsedKey)
}
private class Dummy(val notional: Amount<Currency>)
@Test
fun readAmount() {
val oldJson = """
{
"notional": {
"quantity": 2500000000,
"token": "USD"
}
}
"""
val newJson = """ { "notional" : "$25000000" } """
assertEquals(Amount(2500000000L, USD), mapper.readValue(newJson, Dummy::class.java).notional)
assertEquals(Amount(2500000000L, USD), mapper.readValue(oldJson, Dummy::class.java).notional)
}
@Test
fun writeAmount() {
val writer = mapper.writer().without(SerializationFeature.INDENT_OUTPUT)
assertEquals("""{"notional":"25000000.00 USD"}""", writer.writeValueAsString(Dummy(Amount.parseCurrency("$25000000"))))
}
}

View File

@ -3,6 +3,17 @@ Changelog
Here are brief summaries of what's changed between each snapshot release.
UNRELEASED
----------
API changes:
* The new Jackson module provides JSON/YAML serialisers for common Corda datatypes. If you have previously been
using the JSON support in the standalone web server, please be aware that amounts are now serialised as strings
instead of { quantity, token } pairs as before. The old format is still accepted, but new JSON will be produced
using strings like "1000.00 USD" when writing. You can use any format supported by ``Amount.parseCurrency``
as input.
Milestone 9.1
-------------

View File

@ -1,10 +1,7 @@
{
"fixedLeg": {
"fixedRatePayer": "fixedRatePayerKey",
"notional": {
"quantity": 2500000000,
"token": "EUR"
},
"notional": "€25000000",
"paymentFrequency": "SemiAnnual",
"effectiveDate": "2016-03-11",
"effectiveDateAdjustment": null,
@ -26,10 +23,7 @@
},
"floatingLeg": {
"floatingRatePayer": "floatingRatePayerKey",
"notional": {
"quantity": 2500000000,
"token": "EUR"
},
"notional": "€25000000",
"paymentFrequency": "Quarterly",
"effectiveDate": "2016-03-11",
"effectiveDateAdjustment": null,
@ -66,22 +60,10 @@
"baseCurrency": "EUR",
"eligibleCurrency": "EUR",
"eligibleCreditSupport": "Cash in an Eligible Currency",
"independentAmounts": {
"quantity": 0,
"token": "EUR"
},
"threshold": {
"quantity": 0,
"token": "EUR"
},
"minimumTransferAmount": {
"quantity": 25000000,
"token": "EUR"
},
"rounding": {
"quantity": 1000000,
"token": "EUR"
},
"independentAmounts": "0 EUR",
"threshold": "0 EUR",
"minimumTransferAmount": "250000 EUR",
"rounding": "10000 EUR",
"valuationDateDescription": "Every Local Business Day",
"notificationTime": "2:00pm London",
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",

View File

@ -1,10 +1,7 @@
{
"fixedLeg": {
"fixedRatePayer": "3ThWzJauCq7qLrcX4KuKHxKnxZ6HoxnxU7pFL1HwfCkCJLUfTJ9zN92oxRLxnw",
"notional": {
"quantity": 2500000000,
"token": "USD"
},
"notional": "$25000000",
"paymentFrequency": "SemiAnnual",
"effectiveDate": "2016-03-11",
"effectiveDateAdjustment": null,
@ -66,22 +63,10 @@
"baseCurrency": "EUR",
"eligibleCurrency": "EUR",
"eligibleCreditSupport": "Cash in an Eligible Currency",
"independentAmounts": {
"quantity": 0,
"token": "EUR"
},
"threshold": {
"quantity": 0,
"token": "EUR"
},
"minimumTransferAmount": {
"quantity": 25000000,
"token": "EUR"
},
"rounding": {
"quantity": 1000000,
"token": "EUR"
},
"independentAmounts": "€0",
"threshold": "€0",
"minimumTransferAmount": "250000 EUR",
"rounding": "10000 EUR",
"valuationDateDescription": "Every Local Business Day",
"notificationTime": "2:00pm London",
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",