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

Documentation of exception-related contracts #1980

Merged
merged 4 commits into from
Jul 5, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions core/commonMain/src/kotlinx/serialization/KSerializer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ import kotlinx.serialization.encoding.*
* ```
*
* Deserialization process is symmetric and uses [Decoder].
*
* ### Exception types for `KSerializer` implementation
*
* Implementations of [serialize] and [deserialize] methods are allowed to throw
* any subtype of [IllegalArgumentException] in order to indicate serialization
* and deserialization errors.
*
* For serializer implementations, it is recommended to throw subclasses of [SerializationException] for
* any serialization-specific errors related to invalid or unsupported format of the data
* and [IllegalStateException] for errors during validation of the data.
*
*/
public interface KSerializer<T> : SerializationStrategy<T>, DeserializationStrategy<T> {
/**
Expand Down Expand Up @@ -106,6 +117,10 @@ public interface SerializationStrategy<in T> {
* // don't encode 'alwaysZero' property because we decided to do so
* } // end of the structure
* ```
*
* @throws SerializationException in case of any serialization-specific error
* @throws IllegalArgumentException if the supplied input does not comply encoder's specification
* @see KSerializer for additional information about general contracts and exception specifics
*/
public fun serialize(encoder: Encoder, value: T)
}
Expand Down Expand Up @@ -171,6 +186,10 @@ public interface DeserializationStrategy<T> {
* return MyData(int, list, alwaysZero = 0L)
* }
* ```
*
* @throws SerializationException in case of any deserialization-specific error
* @throws IllegalArgumentException if the decoded input is not a valid instance of [T]
* @see KSerializer for additional information about general contracts and exception specifics
*/
public fun deserialize(decoder: Decoder): T
}
Expand Down
40 changes: 30 additions & 10 deletions core/commonMain/src/kotlinx/serialization/SerialFormat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ import kotlinx.serialization.modules.*
* Typically, formats have their specific [Encoder] and [Decoder] implementations
* as private classes and do not expose them.
*
* ### Exception types for `SerialFormat` implementation
*
* Methods responsible for format-specific encoding and decoding are allowed to throw
* any subtype of [IllegalArgumentException] in order to indicate serialization
* and deserialization errors. It is recommended to throw subtypes of [SerializationException]
* for encoder and decoder specific errors and [IllegalArgumentException] for input
* and output validation-specific errors.
*
* For formats
*
* ### Not stable for inheritance
*
* `SerialFormat` interface is not stable for inheritance in 3rd party libraries, as new methods
Expand Down Expand Up @@ -49,11 +59,17 @@ public interface BinaryFormat : SerialFormat {

/**
* Serializes and encodes the given [value] to byte array using the given [serializer].
*
* @throws SerializationException in case of any encoding-specific error
* @throws IllegalArgumentException if the encoded input does not comply format's specification
*/
public fun <T> encodeToByteArray(serializer: SerializationStrategy<T>, value: T): ByteArray

/**
* Decodes and deserializes the given [byte array][bytes] to the value of type [T] using the given [deserializer]
* Decodes and deserializes the given [byte array][bytes] to the value of type [T] using the given [deserializer].
*
* @throws SerializationException in case of any decoding-specific error
* @throws IllegalArgumentException if the decoded input is not a valid instance of [T]
*/
public fun <T> decodeFromByteArray(deserializer: DeserializationStrategy<T>, bytes: ByteArray): T
}
Expand All @@ -72,27 +88,37 @@ public interface StringFormat : SerialFormat {

/**
* Serializes and encodes the given [value] to string using the given [serializer].
*
* @throws SerializationException in case of any encoding-specific error
* @throws IllegalArgumentException if the encoded input does not comply format's specification
*/
public fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String

/**
* Decodes and deserializes the given [string] to the value of type [T] using the given [deserializer]
* Decodes and deserializes the given [string] to the value of type [T] using the given [deserializer].
*
* @throws SerializationException in case of any decoding-specific error
* @throws IllegalArgumentException if the decoded input is not a valid instance of [T]
*/
public fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T
}

/**
* Serializes and encodes the given [value] to string using serializer retrieved from the reified type parameter.
*
* @throws SerializationException in case of any encoding-specific error
* @throws IllegalArgumentException if the encoded input does not comply format's specification
*/
@OptIn(ExperimentalSerializationApi::class)
public inline fun <reified T> StringFormat.encodeToString(value: T): String =
encodeToString(serializersModule.serializer(), value)

/**
* Decodes and deserializes the given [string] to the value of type [T] using deserializer
* retrieved from the reified type parameter.
*
* @throws SerializationException in case of any decoding-specific error
* @throws IllegalArgumentException if the decoded input is not a valid instance of [T]
*/
@OptIn(ExperimentalSerializationApi::class)
public inline fun <reified T> StringFormat.decodeFromString(string: String): T =
decodeFromString(serializersModule.serializer(), string)

Expand All @@ -105,7 +131,6 @@ public inline fun <reified T> StringFormat.decodeFromString(string: String): T =
* only applies transformation to the resulting array. It is recommended to use for debugging and
* testing purposes.
*/
@OptIn(ExperimentalSerializationApi::class)
public fun <T> BinaryFormat.encodeToHexString(serializer: SerializationStrategy<T>, value: T): String =
InternalHexConverter.printHexBinary(encodeToByteArray(serializer, value), lowerCase = true)

Expand All @@ -115,7 +140,6 @@ public fun <T> BinaryFormat.encodeToHexString(serializer: SerializationStrategy<
*
* This method is a counterpart to [encodeToHexString]
*/
@OptIn(ExperimentalSerializationApi::class)
public fun <T> BinaryFormat.decodeFromHexString(deserializer: DeserializationStrategy<T>, hex: String): T =
decodeFromByteArray(deserializer, InternalHexConverter.parseHexBinary(hex))

Expand All @@ -127,7 +151,6 @@ public fun <T> BinaryFormat.decodeFromHexString(deserializer: DeserializationStr
* only applies transformation to the resulting array. It is recommended to use for debugging and
* testing purposes.
*/
@OptIn(ExperimentalSerializationApi::class)
public inline fun <reified T> BinaryFormat.encodeToHexString(value: T): String =
encodeToHexString(serializersModule.serializer(), value)

Expand All @@ -137,22 +160,19 @@ public inline fun <reified T> BinaryFormat.encodeToHexString(value: T): String =
*
* This method is a counterpart to [encodeToHexString]
*/
@OptIn(ExperimentalSerializationApi::class)
public inline fun <reified T> BinaryFormat.decodeFromHexString(hex: String): T =
decodeFromHexString(serializersModule.serializer(), hex)

/**
* Serializes and encodes the given [value] to byte array using serializer
* retrieved from the reified type parameter.
*/
@OptIn(ExperimentalSerializationApi::class)
public inline fun <reified T> BinaryFormat.encodeToByteArray(value: T): ByteArray =
encodeToByteArray(serializersModule.serializer(), value)

/**
* Decodes and deserializes the given [byte array][bytes] to the value of type [T] using deserializer
* retrieved from the reified type parameter.
*/
@OptIn(ExperimentalSerializationApi::class)
public inline fun <reified T> BinaryFormat.decodeFromByteArray(bytes: ByteArray): T =
decodeFromByteArray(serializersModule.serializer(), bytes)
Original file line number Diff line number Diff line change
Expand Up @@ -6,36 +6,34 @@ package kotlinx.serialization

/**
* A generic exception indicating the problem in serialization or deserialization process.
* This is a generic exception type that can be thrown during the problem at any stage of the serialization,
* including encoding, decoding, serialization, deserialization.
*
* This is a generic exception type that can be thrown during problems at any stage of the serialization,
* including encoding, decoding, serialization, deserialization, and validation.
* [SerialFormat] implementors should throw subclasses of this exception at any unexpected event,
* whether it is a malformed input or unsupported class layout.
*
* [SerializationException] is a subclass of [IllegalArgumentException] for the sake of consistency and user-defined validation:
* Any serialization exception is triggered by the illegal input, whether
* it is a serializer that does not support specific structure or an invalid input.
*
* It is also an established pattern to validate input in user's classes in the following manner:
* ```
* @Serializable
* class Foo(...) {
* init {
* required(age > 0) { ... }
* require(name.isNotBlank()) { ... }
* }
* }
* ```
* While clearly being serialization error (when compromised data was deserialized),
* Kotlin way is to throw `IllegalArgumentException` here instead of using library-specific `SerializationException`.
*
* For general "catch-all" patterns around deserialization of potentially
* untrusted/invalid/corrupted data it is recommended to catch `IllegalArgumentException` type
* to avoid catching irrelevant to serializaton errors such as `OutOfMemoryError` or domain-specific ones.
*/
public open class SerializationException : IllegalArgumentException {
/*
* Rationale behind making it IllegalArgumentException:
* Any serialization exception is triggered by the illegal argument, whether
* it is a serializer that does not support specific structure or an invalid input.
* Making it IAE just aligns the implementation with this fact.
*
* Another point is input validation. The simplest way to validate
* deserialized data is `require` in `init` block:
* ```
* @Serializable class Foo(...) {
* init {
* required(age > 0) { ... }
* require(name.isNotBlank()) { ... }
* }
* }
* ```
* While clearly being serialization error (when compromised data was deserialized),
* Kotlin way is to throw IAE here instead of using library-specific SerializationException.
*
* Also, any production-grade system has a general try-catch around deserialization of potentially
* untrusted/invalid/corrupted data with the corresponding logging, error reporting and diagnostic.
* Such handling should catch some subtype of exception (e.g. it's unlikely that catching OOM is desirable).
* Taking it into account, it becomes clear that SE should be subtype of IAE.
*/

/**
* Creates an instance of [SerializationException] without any details.
Expand Down
11 changes: 6 additions & 5 deletions formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ public sealed class Json(
/**
* Deserializes the given JSON [string] into a value of type [T] using the given [deserializer].
*
* @throws [SerializationException] if the given JSON string cannot be deserialized to the value of type [T].
* @throws [SerializationException] if the given JSON string is not a valid JSON input for the type [T]
* @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
*/
public final override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T {
val lexer = StringJsonLexer(string)
Expand All @@ -98,7 +99,7 @@ public sealed class Json(
/**
* Serializes the given [value] into an equivalent [JsonElement] using the given [serializer]
*
* @throws [SerializationException] if the given value cannot be serialized.
* @throws [SerializationException] if the given value cannot be serialized to JSON
*/
public fun <T> encodeToJsonElement(serializer: SerializationStrategy<T>, value: T): JsonElement {
return writeJson(value, serializer)
Expand All @@ -107,7 +108,8 @@ public sealed class Json(
/**
* Deserializes the given [element] into a value of type [T] using the given [deserializer].
*
* @throws [SerializationException] if the given JSON string cannot be deserialized to the value of type [T].
* @throws [SerializationException] if the given JSON element is not a valid JSON input for the type [T]
* @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
*/
public fun <T> decodeFromJsonElement(deserializer: DeserializationStrategy<T>, element: JsonElement): T {
return readJson(element, deserializer)
Expand All @@ -116,7 +118,7 @@ public sealed class Json(
/**
* Deserializes the given JSON [string] into a corresponding [JsonElement] representation.
*
* @throws [SerializationException] if the given JSON string is malformed and cannot be deserialized
* @throws [SerializationException] if the given string is not a valid JSON
*/
public fun parseToJsonElement(string: String): JsonElement {
return decodeFromString(JsonElementSerializer, string)
Expand Down Expand Up @@ -180,7 +182,6 @@ public enum class DecodeSequenceMode {
/**
* Creates an instance of [Json] configured from the optionally given [Json instance][from] and adjusted with [builderAction].
*/
@OptIn(ExperimentalSerializationApi::class)
public fun Json(from: Json = Json.Default, builderAction: JsonBuilder.() -> Unit): Json {
val builder = JsonBuilder(from)
builder.builderAction()
Expand Down