From ae5b9de484f463e7bdf2ca1c4373cddc9e7428b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20M=C3=A9lois?= Date: Wed, 9 Oct 2024 10:11:00 +0200 Subject: [PATCH] Lenient numeric decoding (#1606) * Add a flag to allow for numerics to be decoded from JSON strings * Expose JSON codec transformations to SimpleRestJsonBuilder * Add mention in docs how JSON codec transformations are exposed --- CHANGELOG.md | 1 + .../04-simple-rest-json/01-overview.md | 8 + .../http4s/SimpleRestJsonBuilder.scala | 42 +++- .../internals/SimpleRestJsonCodecs.scala | 25 +- .../smithy4s/json/JsoniterCodecCompiler.scala | 6 + .../internals/JsoniterCodecCompilerImpl.scala | 8 +- .../json/internals/SchemaVisitorJCodec.scala | 221 +++++++++++------- ...chemaVisitorJCodecCustomisationTests.scala | 59 +++++ 8 files changed, 260 insertions(+), 110 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9c3a4fa4..a3909041d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ Thank you! # 0.18.25 +* Add A flag to allow for numerics to be decoded from JSON strings (in smithy4s-json). * Fixes issues in which applications of some Smithy traits would be incorrectly rendered in Scala code (see [#1602](https://github.com/disneystreaming/smithy4s/pull/1602)). * Fixes an issue in which refinements wouldn't work on custom simple shapes (newtypes) (see [#1595](https://github.com/disneystreaming/smithy4s/pull/1595)) * Fixes a regression from 0.18.4 which incorrectly rendered default values for certain types (see [#1593](https://github.com/disneystreaming/smithy4s/pull/1593)) diff --git a/modules/docs/markdown/03-protocols/04-simple-rest-json/01-overview.md b/modules/docs/markdown/03-protocols/04-simple-rest-json/01-overview.md index 1726a52f4..f57de12c0 100644 --- a/modules/docs/markdown/03-protocols/04-simple-rest-json/01-overview.md +++ b/modules/docs/markdown/03-protocols/04-simple-rest-json/01-overview.md @@ -90,6 +90,14 @@ See the section about [unions](../../04-codegen/02-unions.md) for a detailed des By default, optional properties (headers, query parameters, structure fields) that are set to `None` and optional properties that are set to default value will be excluded during encoding process. If you wish to change this so that instead they are included and set to `null` explicitly, you can do so by calling `.withExplicitDefaultsEncoding(true)`. +## Other customisations of JSON codec behaviour + +The underlying JSON codecs can be configured with a number of options to cater to niche usecases, via the `.transformJsonCodecs` method, which takes a function that takes in and returns a +`JsonPayloadCodecCompiler`. For instance, by default, the `NaN` and `Infinity` values are not considered valid during parsing `Float` or `Double` values. This can be amended via +`.transformJsonCodecs(_.configureJsoniterCodecCompiler(_.withInfinitySupport(true)))`. + +The customisations are bound to evolve as we uncover new niche cases that warrant adding new pieces of opt-in behaviour. The default behaviour is kept rather strict as it helps keep competitive performance and safety. + ## Supported traits Here is the list of traits supported by `SimpleRestJson` diff --git a/modules/http4s/src/smithy4s/http4s/SimpleRestJsonBuilder.scala b/modules/http4s/src/smithy4s/http4s/SimpleRestJsonBuilder.scala index f4c76f516..6d3e96226 100644 --- a/modules/http4s/src/smithy4s/http4s/SimpleRestJsonBuilder.scala +++ b/modules/http4s/src/smithy4s/http4s/SimpleRestJsonBuilder.scala @@ -17,7 +17,17 @@ package smithy4s package http4s -object SimpleRestJsonBuilder extends SimpleRestJsonBuilder(1024, false, true) +import smithy4s.json.Json +import smithy4s.json.JsonPayloadCodecCompiler + +object SimpleRestJsonBuilder + extends SimpleRestJsonBuilder( + new internals.SimpleRestJsonCodecs( + jsonCodecs = Json.payloadCodecs, + explicitDefaultsEncoding = false, + hostPrefixInjection = true + ) + ) class SimpleRestJsonBuilder private ( simpleRestJsonCodecs: internals.SimpleRestJsonCodecs @@ -25,6 +35,7 @@ class SimpleRestJsonBuilder private ( simpleRestJsonCodecs ) { + @deprecated(message = "Use .withXXX methods instead", since = "0.18.25") def this( maxArity: Int, explicitDefaultsEncoding: Boolean, @@ -32,7 +43,12 @@ class SimpleRestJsonBuilder private ( ) = this( new internals.SimpleRestJsonCodecs( - maxArity, + Json.payloadCodecs + .withJsoniterCodecCompiler( + Json.jsoniter + .withMaxArity(maxArity) + .withExplicitDefaultsEncoding(explicitDefaultsEncoding) + ), explicitDefaultsEncoding, hostPrefixInjection ) @@ -40,24 +56,28 @@ class SimpleRestJsonBuilder private ( def withMaxArity(maxArity: Int): SimpleRestJsonBuilder = new SimpleRestJsonBuilder( - maxArity, - simpleRestJsonCodecs.explicitDefaultsEncoding, - simpleRestJsonCodecs.hostPrefixInjection + simpleRestJsonCodecs.transformJsonCodecs( + _.configureJsoniterCodecCompiler(_.withMaxArity(maxArity)) + ) ) def withExplicitDefaultsEncoding( explicitDefaultsEncoding: Boolean ): SimpleRestJsonBuilder = new SimpleRestJsonBuilder( - simpleRestJsonCodecs.maxArity, - explicitDefaultsEncoding, - simpleRestJsonCodecs.hostPrefixInjection + simpleRestJsonCodecs.withExplicitDefaultEncoding(explicitDefaultsEncoding) ) def disableHostPrefixInjection(): SimpleRestJsonBuilder = new SimpleRestJsonBuilder( - simpleRestJsonCodecs.maxArity, - simpleRestJsonCodecs.explicitDefaultsEncoding, - false + simpleRestJsonCodecs.withHostPrefixInjection(false) ) + + /** + * Transforms the underlying JSON codec compiler to change its behaviour. + */ + def transformJsonCodecs( + f: JsonPayloadCodecCompiler => JsonPayloadCodecCompiler + ): SimpleRestJsonBuilder = + new SimpleRestJsonBuilder(simpleRestJsonCodecs.transformJsonCodecs(f)) } diff --git a/modules/http4s/src/smithy4s/http4s/internals/SimpleRestJsonCodecs.scala b/modules/http4s/src/smithy4s/http4s/internals/SimpleRestJsonCodecs.scala index b7f7648fa..da153c842 100644 --- a/modules/http4s/src/smithy4s/http4s/internals/SimpleRestJsonCodecs.scala +++ b/modules/http4s/src/smithy4s/http4s/internals/SimpleRestJsonCodecs.scala @@ -24,7 +24,6 @@ import smithy4s.http.HttpDiscriminator import smithy4s.http.Metadata import smithy4s.http._ import smithy4s.http4s.kernel._ -import smithy4s.json.Json import smithy4s.client._ import smithy4s.codecs.BlobEncoder import cats.syntax.all._ @@ -32,30 +31,36 @@ import org.http4s.Response import org.http4s.Request import org.http4s.Uri import smithy4s.http.HttpMethod +import smithy4s.json.JsonPayloadCodecCompiler // scalafmt: {maxColumn = 120} private[http4s] class SimpleRestJsonCodecs( - val maxArity: Int, + val jsonCodecs: JsonPayloadCodecCompiler, val explicitDefaultsEncoding: Boolean, val hostPrefixInjection: Boolean ) extends SimpleProtocolCodecs { private val hintMask = alloy.SimpleRestJson.protocol.hintMask - private val jsonCodecs = Json.payloadCodecs - .withJsoniterCodecCompiler( - Json.jsoniter - .withHintMask(hintMask) - .withMaxArity(maxArity) - .withExplicitDefaultsEncoding(explicitDefaultsEncoding) + def transformJsonCodecs(f: JsonPayloadCodecCompiler => JsonPayloadCodecCompiler): SimpleRestJsonCodecs = + new SimpleRestJsonCodecs(f(jsonCodecs), explicitDefaultsEncoding, hostPrefixInjection) + + def withExplicitDefaultEncoding(newExplicitDefaultsEncoding: Boolean): SimpleRestJsonCodecs = + new SimpleRestJsonCodecs( + jsonCodecs.configureJsoniterCodecCompiler(_.withExplicitDefaultsEncoding(newExplicitDefaultsEncoding)), + newExplicitDefaultsEncoding, + hostPrefixInjection ) + def withHostPrefixInjection(newHostPrefixInjection: Boolean): SimpleRestJsonCodecs = + new SimpleRestJsonCodecs(jsonCodecs, explicitDefaultsEncoding, newHostPrefixInjection) + // val mediaType = HttpMediaType("application/json") private val payloadEncoders: BlobEncoder.Compiler = - jsonCodecs.encoders + jsonCodecs.configureJsoniterCodecCompiler(_.withHintMask(hintMask)).encoders private val payloadDecoders = - jsonCodecs.decoders + jsonCodecs.configureJsoniterCodecCompiler(_.withHintMask(hintMask)).decoders // Adding X-Amzn-Errortype as well to facilitate interop with Amazon-issued code-generators. private val errorHeaders = List( diff --git a/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala b/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala index a8a8ee8a7..fd032f266 100644 --- a/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala +++ b/modules/json/src/smithy4s/json/JsoniterCodecCompiler.scala @@ -78,6 +78,12 @@ trait JsoniterCodecCompiler extends CachedSchemaCompiler[JsonCodec] { */ def withLenientTaggedUnionDecoding: JsoniterCodecCompiler + /** + * Enables lenient decoding of numeric values, where numbers may be carried by JSON strings + * as well as JSON numbers. + */ + def withLenientNumericDecoding: JsoniterCodecCompiler + } object JsoniterCodecCompiler { diff --git a/modules/json/src/smithy4s/json/internals/JsoniterCodecCompilerImpl.scala b/modules/json/src/smithy4s/json/internals/JsoniterCodecCompilerImpl.scala index 09fbc9bc3..92d3afd50 100644 --- a/modules/json/src/smithy4s/json/internals/JsoniterCodecCompilerImpl.scala +++ b/modules/json/src/smithy4s/json/internals/JsoniterCodecCompilerImpl.scala @@ -27,7 +27,8 @@ private[smithy4s] case class JsoniterCodecCompilerImpl( infinitySupport: Boolean, preserveMapOrder: Boolean, hintMask: Option[HintMask], - lenientTaggedUnionDecoding: Boolean + lenientTaggedUnionDecoding: Boolean, + lenientNumericDecoding: Boolean ) extends CachedSchemaCompiler.Impl[JCodec] with JsoniterCodecCompiler { @@ -59,6 +60,9 @@ private[smithy4s] case class JsoniterCodecCompilerImpl( def withLenientTaggedUnionDecoding: JsoniterCodecCompiler = copy(lenientTaggedUnionDecoding = true) + def withLenientNumericDecoding: JsoniterCodecCompiler = + copy(lenientNumericDecoding = true) + def fromSchema[A](schema: Schema[A], cache: Cache): JCodec[A] = { val visitor = new SchemaVisitorJCodec( maxArity, @@ -67,6 +71,7 @@ private[smithy4s] case class JsoniterCodecCompilerImpl( flexibleCollectionsSupport, preserveMapOrder, lenientTaggedUnionDecoding, + lenientNumericDecoding, cache ) val amendedSchema = @@ -88,6 +93,7 @@ private[smithy4s] object JsoniterCodecCompilerImpl { flexibleCollectionsSupport = false, preserveMapOrder = false, lenientTaggedUnionDecoding = false, + lenientNumericDecoding = false, hintMask = Some(JsoniterCodecCompiler.defaultHintMask) ) diff --git a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala index f4df6330c..354b72e81 100644 --- a/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala +++ b/modules/json/src/smithy4s/json/internals/SchemaVisitorJCodec.scala @@ -47,10 +47,14 @@ private[smithy4s] class SchemaVisitorJCodec( flexibleCollectionsSupport: Boolean, preserveMapOrder: Boolean, lenientTaggedUnionDecoding: Boolean, + lenientNumericDecoding: Boolean, val cache: CompilationCache[JCodec] ) extends SchemaVisitor.Cached[JCodec] { self => private val emptyMetadata: MMap[String, Any] = MMap.empty + private val allowJsonStringNumerics = + lenientNumericDecoding || infinitySupport + object PrimitiveJCodecs { val boolean: JCodec[Boolean] = new JCodec[Boolean] { @@ -80,153 +84,194 @@ private[smithy4s] class SchemaVisitorJCodec( def encodeKey(x: String, out: JsonWriter): Unit = out.writeKey(x) } - val int: JCodec[Int] = - new JCodec[Int] { - def expecting: String = "int" + private abstract class NumericJCodec[A] extends JCodec[A] { + def decodeJsonNumber(cursor: Cursor, in: JsonReader): A + // Allows numerics to be received as JSON Strings + def decodeJsonString(cursor: Cursor, in: JsonReader): A - def decodeValue(cursor: Cursor, in: JsonReader): Int = in.readInt() + final def decodeValue(cursor: Cursor, in: JsonReader): A = + if (allowJsonStringNumerics) { + if (in.isNextToken('"')) { + in.rollbackToken() + decodeJsonString(cursor, in) + } else { + in.rollbackToken() + decodeJsonNumber(cursor, in) + } + } else { + decodeJsonNumber(cursor, in) + } + } - def encodeValue(x: Int, out: JsonWriter): Unit = out.writeVal(x) + val int: JCodec[Int] = new NumericJCodec[Int] { + def expecting: String = "int" - def decodeKey(in: JsonReader): Int = in.readKeyAsInt() + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Int = + in.readInt() - def encodeKey(x: Int, out: JsonWriter): Unit = out.writeKey(x) - } + def decodeJsonString(cursor: Cursor, in: JsonReader): Int = + in.readStringAsInt() - val long: JCodec[Long] = - new JCodec[Long] { - def expecting: String = "long" + def encodeValue(x: Int, out: JsonWriter): Unit = out.writeVal(x) - def decodeValue(cursor: Cursor, in: JsonReader): Long = in.readLong() + def decodeKey(in: JsonReader): Int = in.readKeyAsInt() - def encodeValue(x: Long, out: JsonWriter): Unit = out.writeVal(x) + def encodeKey(x: Int, out: JsonWriter): Unit = out.writeKey(x) + } - def decodeKey(in: JsonReader): Long = in.readKeyAsLong() + val long: JCodec[Long] = new NumericJCodec[Long] { + def expecting: String = "long" - def encodeKey(x: Long, out: JsonWriter): Unit = out.writeKey(x) - } + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Long = + in.readLong() - private val efficientFloat: JCodec[Float] = - new JCodec[Float] { - def expecting: String = "float" + def decodeJsonString(cursor: Cursor, in: JsonReader): Long = + in.readStringAsLong() - def decodeValue(cursor: Cursor, in: JsonReader): Float = in.readFloat() + def encodeValue(x: Long, out: JsonWriter): Unit = out.writeVal(x) - def encodeValue(x: Float, out: JsonWriter): Unit = out.writeVal(x) + def decodeKey(in: JsonReader): Long = in.readKeyAsLong() - def decodeKey(in: JsonReader): Float = in.readKeyAsFloat() + def encodeKey(x: Long, out: JsonWriter): Unit = out.writeKey(x) + } - def encodeKey(x: Float, out: JsonWriter): Unit = out.writeKey(x) - } + private val efficientFloat: JCodec[Float] = new NumericJCodec[Float] { + def expecting: String = "float" - private val infinityAllowingFloat: JCodec[Float] = new JCodec[Float] { - val expecting: String = "JSON number for numeric values" + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Float = + in.readFloat() - def decodeValue(cursor: Cursor, in: JsonReader): Float = - if (in.isNextToken('"')) { - in.rollbackToken() + def decodeJsonString(cursor: Cursor, in: JsonReader): Float = + in.readStringAsFloat() + + def encodeValue(x: Float, out: JsonWriter): Unit = out.writeVal(x) + + def decodeKey(in: JsonReader): Float = in.readKeyAsFloat() + + def encodeKey(x: Float, out: JsonWriter): Unit = out.writeKey(x) + } + + private val infinityAllowingFloat: JCodec[Float] = + new NumericJCodec[Float] { + val expecting: String = "JSON number for numeric values" + + def decodeJsonString(cursor: Cursor, in: JsonReader): Float = { + in.setMark() val len = in.readStringAsCharBuf() if (in.isCharBufEqualsTo(len, "NaN")) Float.NaN else if (in.isCharBufEqualsTo(len, "Infinity")) Float.PositiveInfinity else if (in.isCharBufEqualsTo(len, "-Infinity")) Float.NegativeInfinity - else in.decodeError("illegal float") - } else { - in.rollbackToken() + else { + in.rollbackToMark() + in.readStringAsFloat() + } + } + + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Float = { in.readFloat() } - def encodeValue(f: Float, out: JsonWriter): Unit = - if (java.lang.Float.isFinite(f)) out.writeVal(f) - else - out.writeNonEscapedAsciiVal { - if (f != f) "NaN" - else if (f >= 0) "Infinity" - else "-Infinity" - } + def encodeValue(f: Float, out: JsonWriter): Unit = + if (java.lang.Float.isFinite(f)) out.writeVal(f) + else + out.writeNonEscapedAsciiVal { + if (f != f) "NaN" + else if (f >= 0) "Infinity" + else "-Infinity" + } - def decodeKey(in: JsonReader): Float = ??? + def decodeKey(in: JsonReader): Float = ??? - def encodeKey(x: Float, out: JsonWriter): Unit = ??? - } + def encodeKey(x: Float, out: JsonWriter): Unit = ??? + } val float: JCodec[Float] = if (infinitySupport) infinityAllowingFloat else efficientFloat - private val efficientDouble: JCodec[Double] = - new JCodec[Double] { - def expecting: String = "double" + private val efficientDouble: JCodec[Double] = new NumericJCodec[Double] { + def expecting: String = "double" - def decodeValue(cursor: Cursor, in: JsonReader): Double = - in.readDouble() + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Double = + in.readDouble() - def encodeValue(x: Double, out: JsonWriter): Unit = out.writeVal(x) + def decodeJsonString(cursor: Cursor, in: JsonReader): Double = + in.readStringAsDouble() - def decodeKey(in: JsonReader): Double = in.readKeyAsDouble() + def encodeValue(x: Double, out: JsonWriter): Unit = out.writeVal(x) - def encodeKey(x: Double, out: JsonWriter): Unit = out.writeKey(x) - } + def decodeKey(in: JsonReader): Double = in.readKeyAsDouble() + + def encodeKey(x: Double, out: JsonWriter): Unit = out.writeKey(x) + } - private val infinityAllowingDouble: JCodec[Double] = new JCodec[Double] { - val expecting: String = "JSON number for numeric values" + private val infinityAllowingDouble: JCodec[Double] = + new NumericJCodec[Double] { + val expecting: String = "JSON number for numeric values" - def decodeValue(cursor: Cursor, in: JsonReader): Double = - if (in.isNextToken('"')) { - in.rollbackToken() + def decodeJsonString(cursor: Cursor, in: JsonReader): Double = { + in.setMark() val len = in.readStringAsCharBuf() if (in.isCharBufEqualsTo(len, "NaN")) Double.NaN else if (in.isCharBufEqualsTo(len, "Infinity")) Double.PositiveInfinity else if (in.isCharBufEqualsTo(len, "-Infinity")) Double.NegativeInfinity - else in.decodeError("illegal double") - } else { - in.rollbackToken() - in.readDouble() + else { + in.rollbackToMark() + in.readStringAsDouble() + } } - def encodeValue(d: Double, out: JsonWriter): Unit = - if (java.lang.Double.isFinite(d)) out.writeVal(d) - else - out.writeNonEscapedAsciiVal { - if (d != d) "NaN" - else if (d >= 0) "Infinity" - else "-Infinity" - } + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Double = + in.readDouble() - def decodeKey(in: JsonReader): Double = ??? + def encodeValue(d: Double, out: JsonWriter): Unit = + if (java.lang.Double.isFinite(d)) out.writeVal(d) + else + out.writeNonEscapedAsciiVal { + if (d != d) "NaN" + else if (d >= 0) "Infinity" + else "-Infinity" + } - def encodeKey(x: Double, out: JsonWriter): Unit = ??? - } + def decodeKey(in: JsonReader): Double = ??? + + def encodeKey(x: Double, out: JsonWriter): Unit = ??? + } val double: JCodec[Double] = if (infinitySupport) infinityAllowingDouble else efficientDouble - val short: JCodec[Short] = - new JCodec[Short] { - def expecting: String = "short" + val short: JCodec[Short] = new NumericJCodec[Short] { + def expecting: String = "short" - def decodeValue(cursor: Cursor, in: JsonReader): Short = in.readShort() + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Short = + in.readShort() - def encodeValue(x: Short, out: JsonWriter): Unit = out.writeVal(x) + def decodeJsonString(cursor: Cursor, in: JsonReader): Short = + in.readStringAsShort() - def decodeKey(in: JsonReader): Short = in.readKeyAsShort() + def encodeValue(x: Short, out: JsonWriter): Unit = out.writeVal(x) - def encodeKey(x: Short, out: JsonWriter): Unit = out.writeKey(x) - } + def decodeKey(in: JsonReader): Short = in.readKeyAsShort() - val byte: JCodec[Byte] = - new JCodec[Byte] { - def expecting: String = "byte" + def encodeKey(x: Short, out: JsonWriter): Unit = out.writeKey(x) + } - def decodeValue(cursor: Cursor, in: JsonReader): Byte = in.readByte() + val byte: JCodec[Byte] = new NumericJCodec[Byte] { + def expecting: String = "byte" - def encodeValue(x: Byte, out: JsonWriter): Unit = out.writeVal(x) + def decodeJsonNumber(cursor: Cursor, in: JsonReader): Byte = in.readByte() - def decodeKey(in: JsonReader): Byte = in.readKeyAsByte() + def decodeJsonString(cursor: Cursor, in: JsonReader): Byte = in.readByte() - def encodeKey(x: Byte, out: JsonWriter): Unit = out.writeKey(x) - } + def encodeValue(x: Byte, out: JsonWriter): Unit = out.writeVal(x) + + def decodeKey(in: JsonReader): Byte = in.readKeyAsByte() + + def encodeKey(x: Byte, out: JsonWriter): Unit = out.writeKey(x) + } val bytes: JCodec[Blob] = new JCodec[Blob] { diff --git a/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecCustomisationTests.scala b/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecCustomisationTests.scala index 398615baf..c0d9ac9f0 100644 --- a/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecCustomisationTests.scala +++ b/modules/json/test/src/smithy4s/json/SchemaVisitorJCodecCustomisationTests.scala @@ -24,6 +24,30 @@ import munit._ class SchemaVisitorJCodecCustomisationTests extends FunSuite { + case class FooInt(i: Int) + object FooInt { + implicit val schema: Schema[FooInt] = { + val i = int.required[FooInt]("i", _.i) + struct(i)(FooInt.apply) + } + } + + case class FooShort(s: Short) + object FooShort { + implicit val schema: Schema[FooShort] = { + val s = short.required[FooShort]("s", _.s) + struct(s)(FooShort.apply) + } + } + + case class FooLong(l: Long) + object FooLong { + implicit val schema: Schema[FooLong] = { + val s = long.required[FooLong]("l", _.l) + struct(s)(FooLong.apply) + } + } + case class FooDouble(d: Double) object FooDouble { implicit val schema: Schema[FooDouble] = { @@ -178,4 +202,39 @@ class SchemaVisitorJCodecCustomisationTests extends FunSuite { expect(result.f.isNegInfinity) } + + test("decoding JSON string as a Float") { + val input = """{"f" : "1.1" }""" + val result = readFromString[FooFloat](input) + + expect.eql(result.f, 1.1f) + } + + test("decoding JSON string as a Double") { + val input = """{"d" : "1.1" }""" + val result = readFromString[FooDouble](input) + + expect.eql(result.d, 1.1d) + } + + test("decoding JSON string as an Int") { + val input = """{"i" : "1" }""" + val result = readFromString[FooInt](input) + + expect.eql(result.i, 1) + } + + test("decoding JSON string as a Long") { + val input = """{"l" : "1" }""" + val result = readFromString[FooLong](input) + + expect.eql(result.l, 1L) + } + + test("decoding JSON string as a Short") { + val input = """{"s" : "1" }""" + val result = readFromString[FooShort](input) + + expect.eql(result.s, 1.toShort) + } }