Skip to content

Commit

Permalink
Documentation of exception-related contracts (#1980)
Browse files Browse the repository at this point in the history
* Add general exception contract for KSerializer, improve documentation of SerializationExceptions to make it more KDoc-friendly
* Add contracts to formats and their extensions

Fixes #1875
  • Loading branch information
qwwdfsad committed Jul 5, 2022
1 parent 0f1034e commit be99c0d
Show file tree
Hide file tree
Showing 6 changed files with 117 additions and 55 deletions.
19 changes: 19 additions & 0 deletions core/commonMain/src/kotlinx/serialization/KSerializer.kt
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
62 changes: 50 additions & 12 deletions core/commonMain/src/kotlinx/serialization/SerialFormat.kt
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 @@ -104,18 +130,22 @@ public inline fun <reified T> StringFormat.decodeFromString(string: String): T =
* Hex representation does not interfere with serialization and encoding process of the format and
* only applies transformation to the resulting array. It is recommended to use for debugging and
* testing purposes.
*
* @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 fun <T> BinaryFormat.encodeToHexString(serializer: SerializationStrategy<T>, value: T): String =
InternalHexConverter.printHexBinary(encodeToByteArray(serializer, value), lowerCase = true)

/**
* Decodes byte array from the given [hex] string and the decodes and deserializes it
* to the value of type [T], delegating it to the [BinaryFormat].
*
* This method is a counterpart to [encodeToHexString]
* This method is a counterpart to [encodeToHexString].
*
* @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 fun <T> BinaryFormat.decodeFromHexString(deserializer: DeserializationStrategy<T>, hex: String): T =
decodeFromByteArray(deserializer, InternalHexConverter.parseHexBinary(hex))

Expand All @@ -126,33 +156,41 @@ public fun <T> BinaryFormat.decodeFromHexString(deserializer: DeserializationStr
* Hex representation does not interfere with serialization and encoding process of the format and
* only applies transformation to the resulting array. It is recommended to use for debugging and
* testing purposes.
*
* @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> BinaryFormat.encodeToHexString(value: T): String =
encodeToHexString(serializersModule.serializer(), value)

/**
* Decodes byte array from the given [hex] string and the decodes and deserializes it
* to the value of type [T], delegating it to the [BinaryFormat].
*
* This method is a counterpart to [encodeToHexString]
* This method is a counterpart to [encodeToHexString].
*
* @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> 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.
*
* @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> 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.
*
* @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> BinaryFormat.decodeFromByteArray(bytes: ByteArray): T =
decodeFromByteArray(serializersModule.serializer(), bytes)
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
14 changes: 8 additions & 6 deletions formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
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 All @@ -202,7 +203,8 @@ public inline fun <reified T> Json.encodeToJsonElement(value: T): JsonElement {
* Deserializes the given [json] element into a value of type [T] using a deserializer retrieved
* from reified type parameter.
*
* @throws [SerializationException] if the given JSON string is malformed or 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 inline fun <reified T> Json.decodeFromJsonElement(json: JsonElement): T =
decodeFromJsonElement(serializersModule.serializer(), json)
Expand Down
Expand Up @@ -4,25 +4,23 @@ import kotlinx.serialization.*
import kotlinx.serialization.json.DecodeSequenceMode
import kotlinx.serialization.json.Json



/** @suppress */
@InternalSerializationApi
public interface JsonWriter {
public fun writeLong(value: Long)
public fun writeChar(char: Char)

public fun write(text: String)

public fun writeQuoted(text: String)

public fun release()
}

/** @suppress */
@InternalSerializationApi
public interface SerialReader {
public fun read(buffer: CharArray, bufferOffset: Int, count: Int): Int
}

/** @suppress */
@InternalSerializationApi
public fun <T> Json.encodeByWriter(writer: JsonWriter, serializer: SerializationStrategy<T>, value: T) {
val encoder = StreamingJsonEncoder(
Expand All @@ -33,6 +31,7 @@ public fun <T> Json.encodeByWriter(writer: JsonWriter, serializer: Serialization
encoder.encodeSerializableValue(serializer, value)
}

/** @suppress */
@InternalSerializationApi
public fun <T> Json.decodeByReader(
deserializer: DeserializationStrategy<T>,
Expand All @@ -45,6 +44,7 @@ public fun <T> Json.decodeByReader(
return result
}

/** @suppress */
@InternalSerializationApi
@ExperimentalSerializationApi
public fun <T> Json.decodeToSequenceByReader(
Expand All @@ -57,6 +57,7 @@ public fun <T> Json.decodeToSequenceByReader(
return Sequence { iter }.constrainOnce()
}

/** @suppress */
@InternalSerializationApi
@ExperimentalSerializationApi
public inline fun <reified T> Json.decodeToSequenceByReader(
Expand Down

0 comments on commit be99c0d

Please sign in to comment.