Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support encoding/decoding non-Long numbers #163

Closed
aSemy opened this issue Nov 18, 2022 · 4 comments · Fixed by #171
Closed

Support encoding/decoding non-Long numbers #163

aSemy opened this issue Nov 18, 2022 · 4 comments · Fixed by #171

Comments

@aSemy
Copy link
Collaborator

aSemy commented Nov 18, 2022

Currently when I try to decode an integer to a Int, I get an error. This is inconvenient, and confusing.

Exception in thread "main" com.akuleshov7.ktoml.exceptions.IllegalTypeException: 
Line 2: <Int> type is not allowed by toml specification, use <Long> instead (key = port; value = 8080)
import com.akuleshov7.ktoml.Toml
import kotlinx.serialization.Serializable

fun main() {
  Toml.decodeFromString(ServerConfig.serializer(), """
    host = "localhost"
    port = 8080
  """.trimIndent())
}

@Serializable
data class ServerConfig(
  val host: String,
  val port: Int,
)

Comparison with Kotlinx Serialization

This is confusing because it does not match the behaviour of Kotlinx Serialization JSON. Technically, the JSON spec states that any number can be of any magnitude or precision (so basically KxS should only support BigDecimal or BigInteger), but KxS allows for numbers to be en/decoded with any numeric type - Long, Short, Double, Float - including unsigned numbers.

import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

@Serializable
data class ServerConfig(
  val host: String,
  val port: Int, // Int is allowed
)

fun main() {
  val config: ServerConfig = Json.decodeFromString(
    """
    {
      "host": "localhost",
      "port": 123213131321321312313213213132123132131
    }
  """.trimIndent()
  )
}

KxS throws a runtime error when it encounters an invalid number

Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 35: Numeric value overflow at path: $.port
JSON input: {
  "host": "localhost",
  "port": 123213131321321312313213213132123132131
}

Work arounds

Custom Serializer

I have tried to work around this with a custom serializer, but I get the same error.

internal typealias IntAsLong = @Serializable(with = IntAsLongSerializer::class) Int

private object IntAsLongSerializer : KSerializer<Int> {
  override val descriptor: SerialDescriptor =
    PrimitiveSerialDescriptor("IntAsLongSerializer", PrimitiveKind.LONG)

  override fun serialize(encoder: Encoder, value: Int) = encoder.encodeLong(value.toLong())

  override fun deserialize(decoder: Decoder): Int = decoder.decodeLong().toInt()
}

@Serializable
data class ServerConfig(
  val host: String,
  val port: IntAsLong,
)

.toInt()

Manually converting on-the-fly with config.port.toInt() is possible, but inconvenient, especially if the value is used in a lot of places.

Secondary value

I guess it's possible to create a secondary value to expose the actual value...? But this is weird and confusing.

@Serializable
data class ServerConfig(
  @SerialName("port")
  private val portLong: Long = 8080,
  val host: String = "localhost",
) {
  @Transient
  val port: Int = portLong.toInt()
}

Request

I would like ktoml to coerce numbers to the value in the @Serializable class. If an unsuitable value is encountered (for example, it is too large), then ktoml should throw an exception at runtime.

Versions

  • Kotlin 1.7.21
  • Kotlinx Serialization 1.4.1
  • ktoml 0.3.0
@orchestr7
Copy link
Owner

orchestr7 commented Nov 18, 2022

yeah, we thought about that with @NightEule5, TOML standard (as we understand it) prohibits the usage of INT.
But there is absolutely no problem to support it, we just wanted to support standard...

"Arbitrary 64-bit signed integers (from −2^63 to 2^63−1) should be accepted and handled losslessly. If an integer cannot be represented losslessly, an error must be thrown."

Probably my understanding was wrong 🤦

@orchestr7
Copy link
Owner

orchestr7 commented Nov 18, 2022

#153 (comment)

Actually everything will start working if we simply remove this exception. I just wanted to be very straight-forward with spec... 🤣

@orchestr7
Copy link
Owner

orchestr7 commented Nov 22, 2022

Looks like the change should be a little bit more complicated: we will need to support TomlInteger class and use it during parsing with avoiding of overflow: https://stackoverflow.com/questions/74510016/how-to-understand-if-there-was-an-integer-overflow-during-parsing-of-string-with

@aSemy
Copy link
Collaborator Author

aSemy commented Nov 22, 2022

I didn't think it would be that difficult, so maybe I'm missing something :)

  1. The KxS SerialDescriptor requests an Int from the Decoder (implemented by ktoml)
  2. The ktoml decoder will attempt to read a number as a Long (as normal) in fun decodeInt()
  3. Then the ktoml decoder can try to convert the Long to an Int/Short/Byte - and throw an exception if the Long isn't a valid Int/Short/Byte

This is similar to how Kotlinx Serialization JSON does it.

I've made a demo PR

https://github.com/akuleshov7/ktoml/blob/f8cb2e90e3b13a2ba3f14a75edf86d8bdba5bfed/ktoml-core/src/commonMain/kotlin/com/akuleshov7/ktoml/decoders/TomlAbstractDecoder.kt#L39-L44

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants