Skip to content

Commit be99c0d

Browse files
authored
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
1 parent 0f1034e commit be99c0d

File tree

6 files changed

+117
-55
lines changed

6 files changed

+117
-55
lines changed

core/commonMain/src/kotlinx/serialization/KSerializer.kt

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,17 @@ import kotlinx.serialization.encoding.*
5151
* ```
5252
*
5353
* Deserialization process is symmetric and uses [Decoder].
54+
*
55+
* ### Exception types for `KSerializer` implementation
56+
*
57+
* Implementations of [serialize] and [deserialize] methods are allowed to throw
58+
* any subtype of [IllegalArgumentException] in order to indicate serialization
59+
* and deserialization errors.
60+
*
61+
* For serializer implementations, it is recommended to throw subclasses of [SerializationException] for
62+
* any serialization-specific errors related to invalid or unsupported format of the data
63+
* and [IllegalStateException] for errors during validation of the data.
64+
*
5465
*/
5566
public interface KSerializer<T> : SerializationStrategy<T>, DeserializationStrategy<T> {
5667
/**
@@ -106,6 +117,10 @@ public interface SerializationStrategy<in T> {
106117
* // don't encode 'alwaysZero' property because we decided to do so
107118
* } // end of the structure
108119
* ```
120+
*
121+
* @throws SerializationException in case of any serialization-specific error
122+
* @throws IllegalArgumentException if the supplied input does not comply encoder's specification
123+
* @see KSerializer for additional information about general contracts and exception specifics
109124
*/
110125
public fun serialize(encoder: Encoder, value: T)
111126
}
@@ -171,6 +186,10 @@ public interface DeserializationStrategy<T> {
171186
* return MyData(int, list, alwaysZero = 0L)
172187
* }
173188
* ```
189+
*
190+
* @throws SerializationException in case of any deserialization-specific error
191+
* @throws IllegalArgumentException if the decoded input is not a valid instance of [T]
192+
* @see KSerializer for additional information about general contracts and exception specifics
174193
*/
175194
public fun deserialize(decoder: Decoder): T
176195
}

core/commonMain/src/kotlinx/serialization/SerialFormat.kt

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ import kotlinx.serialization.modules.*
1919
* Typically, formats have their specific [Encoder] and [Decoder] implementations
2020
* as private classes and do not expose them.
2121
*
22+
* ### Exception types for `SerialFormat` implementation
23+
*
24+
* Methods responsible for format-specific encoding and decoding are allowed to throw
25+
* any subtype of [IllegalArgumentException] in order to indicate serialization
26+
* and deserialization errors. It is recommended to throw subtypes of [SerializationException]
27+
* for encoder and decoder specific errors and [IllegalArgumentException] for input
28+
* and output validation-specific errors.
29+
*
30+
* For formats
31+
*
2232
* ### Not stable for inheritance
2333
*
2434
* `SerialFormat` interface is not stable for inheritance in 3rd party libraries, as new methods
@@ -49,11 +59,17 @@ public interface BinaryFormat : SerialFormat {
4959

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

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

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

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

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

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

@@ -104,18 +130,22 @@ public inline fun <reified T> StringFormat.decodeFromString(string: String): T =
104130
* Hex representation does not interfere with serialization and encoding process of the format and
105131
* only applies transformation to the resulting array. It is recommended to use for debugging and
106132
* testing purposes.
133+
*
134+
* @throws SerializationException in case of any encoding-specific error
135+
* @throws IllegalArgumentException if the encoded input does not comply format's specification
107136
*/
108-
@OptIn(ExperimentalSerializationApi::class)
109137
public fun <T> BinaryFormat.encodeToHexString(serializer: SerializationStrategy<T>, value: T): String =
110138
InternalHexConverter.printHexBinary(encodeToByteArray(serializer, value), lowerCase = true)
111139

112140
/**
113141
* Decodes byte array from the given [hex] string and the decodes and deserializes it
114142
* to the value of type [T], delegating it to the [BinaryFormat].
115143
*
116-
* This method is a counterpart to [encodeToHexString]
144+
* This method is a counterpart to [encodeToHexString].
145+
*
146+
* @throws SerializationException in case of any decoding-specific error
147+
* @throws IllegalArgumentException if the decoded input is not a valid instance of [T]
117148
*/
118-
@OptIn(ExperimentalSerializationApi::class)
119149
public fun <T> BinaryFormat.decodeFromHexString(deserializer: DeserializationStrategy<T>, hex: String): T =
120150
decodeFromByteArray(deserializer, InternalHexConverter.parseHexBinary(hex))
121151

@@ -126,33 +156,41 @@ public fun <T> BinaryFormat.decodeFromHexString(deserializer: DeserializationStr
126156
* Hex representation does not interfere with serialization and encoding process of the format and
127157
* only applies transformation to the resulting array. It is recommended to use for debugging and
128158
* testing purposes.
159+
*
160+
* @throws SerializationException in case of any encoding-specific error
161+
* @throws IllegalArgumentException if the encoded input does not comply format's specification
129162
*/
130-
@OptIn(ExperimentalSerializationApi::class)
131163
public inline fun <reified T> BinaryFormat.encodeToHexString(value: T): String =
132164
encodeToHexString(serializersModule.serializer(), value)
133165

134166
/**
135167
* Decodes byte array from the given [hex] string and the decodes and deserializes it
136168
* to the value of type [T], delegating it to the [BinaryFormat].
137169
*
138-
* This method is a counterpart to [encodeToHexString]
170+
* This method is a counterpart to [encodeToHexString].
171+
*
172+
* @throws SerializationException in case of any decoding-specific error
173+
* @throws IllegalArgumentException if the decoded input is not a valid instance of [T]
139174
*/
140-
@OptIn(ExperimentalSerializationApi::class)
141175
public inline fun <reified T> BinaryFormat.decodeFromHexString(hex: String): T =
142176
decodeFromHexString(serializersModule.serializer(), hex)
143177

144178
/**
145179
* Serializes and encodes the given [value] to byte array using serializer
146180
* retrieved from the reified type parameter.
181+
*
182+
* @throws SerializationException in case of any encoding-specific error
183+
* @throws IllegalArgumentException if the encoded input does not comply format's specification
147184
*/
148-
@OptIn(ExperimentalSerializationApi::class)
149185
public inline fun <reified T> BinaryFormat.encodeToByteArray(value: T): ByteArray =
150186
encodeToByteArray(serializersModule.serializer(), value)
151187

152188
/**
153189
* Decodes and deserializes the given [byte array][bytes] to the value of type [T] using deserializer
154190
* retrieved from the reified type parameter.
191+
*
192+
* @throws SerializationException in case of any decoding-specific error
193+
* @throws IllegalArgumentException if the decoded input is not a valid instance of [T]
155194
*/
156-
@OptIn(ExperimentalSerializationApi::class)
157195
public inline fun <reified T> BinaryFormat.decodeFromByteArray(bytes: ByteArray): T =
158196
decodeFromByteArray(serializersModule.serializer(), bytes)

core/commonMain/src/kotlinx/serialization/SerializationException.kt renamed to core/commonMain/src/kotlinx/serialization/SerializationExceptions.kt

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,34 @@ package kotlinx.serialization
66

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

4038
/**
4139
* Creates an instance of [SerializationException] without any details.

formats/json/commonMain/src/kotlinx/serialization/json/Json.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ public sealed class Json(
8686
/**
8787
* Deserializes the given JSON [string] into a value of type [T] using the given [deserializer].
8888
*
89-
* @throws [SerializationException] if the given JSON string cannot be deserialized to the value of type [T].
89+
* @throws [SerializationException] if the given JSON string is not a valid JSON input for the type [T]
90+
* @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
9091
*/
9192
public final override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T {
9293
val lexer = StringJsonLexer(string)
@@ -98,7 +99,7 @@ public sealed class Json(
9899
/**
99100
* Serializes the given [value] into an equivalent [JsonElement] using the given [serializer]
100101
*
101-
* @throws [SerializationException] if the given value cannot be serialized.
102+
* @throws [SerializationException] if the given value cannot be serialized to JSON
102103
*/
103104
public fun <T> encodeToJsonElement(serializer: SerializationStrategy<T>, value: T): JsonElement {
104105
return writeJson(value, serializer)
@@ -107,7 +108,8 @@ public sealed class Json(
107108
/**
108109
* Deserializes the given [element] into a value of type [T] using the given [deserializer].
109110
*
110-
* @throws [SerializationException] if the given JSON string cannot be deserialized to the value of type [T].
111+
* @throws [SerializationException] if the given JSON element is not a valid JSON input for the type [T]
112+
* @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
111113
*/
112114
public fun <T> decodeFromJsonElement(deserializer: DeserializationStrategy<T>, element: JsonElement): T {
113115
return readJson(element, deserializer)
@@ -116,7 +118,7 @@ public sealed class Json(
116118
/**
117119
* Deserializes the given JSON [string] into a corresponding [JsonElement] representation.
118120
*
119-
* @throws [SerializationException] if the given JSON string is malformed and cannot be deserialized
121+
* @throws [SerializationException] if the given string is not a valid JSON
120122
*/
121123
public fun parseToJsonElement(string: String): JsonElement {
122124
return decodeFromString(JsonElementSerializer, string)
@@ -180,7 +182,6 @@ public enum class DecodeSequenceMode {
180182
/**
181183
* Creates an instance of [Json] configured from the optionally given [Json instance][from] and adjusted with [builderAction].
182184
*/
183-
@OptIn(ExperimentalSerializationApi::class)
184185
public fun Json(from: Json = Json.Default, builderAction: JsonBuilder.() -> Unit): Json {
185186
val builder = JsonBuilder(from)
186187
builder.builderAction()
@@ -202,7 +203,8 @@ public inline fun <reified T> Json.encodeToJsonElement(value: T): JsonElement {
202203
* Deserializes the given [json] element into a value of type [T] using a deserializer retrieved
203204
* from reified type parameter.
204205
*
205-
* @throws [SerializationException] if the given JSON string is malformed or cannot be deserialized to the value of type [T].
206+
* @throws [SerializationException] if the given JSON element is not a valid JSON input for the type [T]
207+
* @throws [IllegalArgumentException] if the decoded input cannot be represented as a valid instance of type [T]
206208
*/
207209
public inline fun <reified T> Json.decodeFromJsonElement(json: JsonElement): T =
208210
decodeFromJsonElement(serializersModule.serializer(), json)

formats/json/commonMain/src/kotlinx/serialization/json/internal/JsonStreams.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,23 @@ import kotlinx.serialization.*
44
import kotlinx.serialization.json.DecodeSequenceMode
55
import kotlinx.serialization.json.Json
66

7-
8-
7+
/** @suppress */
98
@InternalSerializationApi
109
public interface JsonWriter {
1110
public fun writeLong(value: Long)
1211
public fun writeChar(char: Char)
13-
1412
public fun write(text: String)
15-
1613
public fun writeQuoted(text: String)
17-
1814
public fun release()
1915
}
2016

17+
/** @suppress */
2118
@InternalSerializationApi
2219
public interface SerialReader {
2320
public fun read(buffer: CharArray, bufferOffset: Int, count: Int): Int
2421
}
2522

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

34+
/** @suppress */
3635
@InternalSerializationApi
3736
public fun <T> Json.decodeByReader(
3837
deserializer: DeserializationStrategy<T>,
@@ -45,6 +44,7 @@ public fun <T> Json.decodeByReader(
4544
return result
4645
}
4746

47+
/** @suppress */
4848
@InternalSerializationApi
4949
@ExperimentalSerializationApi
5050
public fun <T> Json.decodeToSequenceByReader(
@@ -57,6 +57,7 @@ public fun <T> Json.decodeToSequenceByReader(
5757
return Sequence { iter }.constrainOnce()
5858
}
5959

60+
/** @suppress */
6061
@InternalSerializationApi
6162
@ExperimentalSerializationApi
6263
public inline fun <reified T> Json.decodeToSequenceByReader(

0 commit comments

Comments
 (0)