summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVsevolod Tolstopyatov <qwwdfsad@gmail.com>2022-07-05 17:37:13 +0200
committerGitHub <noreply@github.com>2022-07-05 18:37:13 +0300
commitbe99c0d780712b47144be528222377575e9b21ff (patch)
tree8b4abe1dd90542edab6a0f2ac2f68d2d87f0aebd
parent0f1034e670a6581cdba54e4aa5cb1257e5884ea9 (diff)
downloadkotlinx.serialization-be99c0d780712b47144be528222377575e9b21ff.tar.gz
Documentation of exception-related contracts (#1980)
* 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
-rw-r--r--core/commonMain/src/kotlinx/serialization/KSerializer.kt19
-rw-r--r--core/commonMain/src/kotlinx/serialization/SerialFormat.kt62
-rw-r--r--core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt (renamed from core/commonMain/src/kotlinx/serialization/SerializationException.kt)50
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/Json.kt14
-rw-r--r--formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonStreams.kt11
-rw-r--r--formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt16
6 files changed, 117 insertions, 55 deletions
diff --git a/core/commonMain/src/kotlinx/serialization/KSerializer.kt b/core/commonMain/src/kotlinx/serialization/KSerializer.kt
index 3b6c8697..2d9def18 100644
--- a/core/commonMain/src/kotlinx/serialization/KSerializer.kt
+++ b/core/commonMain/src/kotlinx/serialization/KSerializer.kt
@@ -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> {
/**
@@ -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)
}
@@ -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
}
diff --git a/core/commonMain/src/kotlinx/serialization/SerialFormat.kt b/core/commonMain/src/kotlinx/serialization/SerialFormat.kt
index e4801a0a..11234b2a 100644
--- a/core/commonMain/src/kotlinx/serialization/SerialFormat.kt
+++ b/core/commonMain/src/kotlinx/serialization/SerialFormat.kt
@@ -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
@@ -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
}
@@ -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)
@@ -104,8 +130,10 @@ 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)
@@ -113,9 +141,11 @@ public fun <T> BinaryFormat.encodeToHexString(serializer: SerializationStrategy<
* 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))
@@ -126,8 +156,10 @@ 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)
@@ -135,24 +167,30 @@ public inline fun <reified T> BinaryFormat.encodeToHexString(value: T): String =
* 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)
diff --git a/core/commonMain/src/kotlinx/serialization/SerializationException.kt b/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt
index 41631f50..9b690509 100644
--- a/core/commonMain/src/kotlinx/serialization/SerializationException.kt
+++ b/core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt
@@ -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.
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
index 04871511..33920939 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/Json.kt
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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()
@@ -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)
diff --git a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonStreams.kt b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonStreams.kt
index 75703249..0cf2b5d7 100644
--- a/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonStreams.kt
+++ b/formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonStreams.kt
@@ -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(
@@ -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>,
@@ -45,6 +44,7 @@ public fun <T> Json.decodeByReader(
return result
}
+/** @suppress */
@InternalSerializationApi
@ExperimentalSerializationApi
public fun <T> Json.decodeToSequenceByReader(
@@ -57,6 +57,7 @@ public fun <T> Json.decodeToSequenceByReader(
return Sequence { iter }.constrainOnce()
}
+/** @suppress */
@InternalSerializationApi
@ExperimentalSerializationApi
public inline fun <reified T> Json.decodeToSequenceByReader(
diff --git a/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt b/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt
index 04d8149b..81bfc563 100644
--- a/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt
+++ b/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt
@@ -12,7 +12,7 @@ import java.io.*
* Serializes the [value] with [serializer] into a [stream] using JSON format and UTF-8 encoding.
*
* @throws [SerializationException] if the given value cannot be serialized to JSON.
- * @throws [IOException] If an I/O error occurs and stream can't be written to.
+ * @throws [IOException] If an I/O error occurs and stream cannot be written to.
*/
@ExperimentalSerializationApi
public fun <T> Json.encodeToStream(
@@ -32,7 +32,7 @@ public fun <T> Json.encodeToStream(
* Serializes given [value] to [stream] using UTF-8 encoding and serializer retrieved from the reified type parameter.
*
* @throws [SerializationException] if the given value cannot be serialized to JSON.
- * @throws [IOException] If an I/O error occurs and stream can't be written to.
+ * @throws [IOException] If an I/O error occurs and stream cannot be written to.
*/
@ExperimentalSerializationApi
public inline fun <reified T> Json.encodeToStream(
@@ -48,7 +48,8 @@ public inline fun <reified T> Json.encodeToStream(
* and throws an exception if there are any dangling bytes after an object.
*
* @throws [SerializationException] if the given JSON input cannot be deserialized to the value of type [T].
- * @throws [IOException] If an I/O error occurs and stream can't be read from.
+ * @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
+ * @throws [IOException] If an I/O error occurs and stream cannot be read from.
*/
@ExperimentalSerializationApi
public fun <T> Json.decodeFromStream(
@@ -66,7 +67,8 @@ public fun <T> Json.decodeFromStream(
* and throws an exception if there are any dangling bytes after an object.
*
* @throws [SerializationException] if the given JSON input cannot be deserialized to the value of type [T].
- * @throws [IOException] If an I/O error occurs and stream can't be read from.
+ * @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
+ * @throws [IOException] If an I/O error occurs and stream cannot be read from.
*/
@ExperimentalSerializationApi
public inline fun <reified T> Json.decodeFromStream(stream: InputStream): T =
@@ -86,7 +88,8 @@ public inline fun <reified T> Json.decodeFromStream(stream: InputStream): T =
* closing it before returned sequence is evaluated completely will result in [IOException] from decoder.
*
* @throws [SerializationException] if the given JSON input cannot be deserialized to the value of type [T].
- * @throws [IOException] If an I/O error occurs and stream can't be read from.
+ * @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
+ * @throws [IOException] If an I/O error occurs and stream cannot be read from.
*/
@ExperimentalSerializationApi
public fun <T> Json.decodeToSequence(
@@ -110,7 +113,8 @@ public fun <T> Json.decodeToSequence(
* closing it before returned sequence is evaluated fully would result in [IOException] from decoder.
*
* @throws [SerializationException] if the given JSON input cannot be deserialized to the value of type [T].
- * @throws [IOException] If an I/O error occurs and stream can't be read from.
+ * @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
+ * @throws [IOException] If an I/O error occurs and stream cannot be read from.
*/
@ExperimentalSerializationApi
public inline fun <reified T> Json.decodeToSequence(