Skip to content

Commit aee6336

Browse files
authored
Introduce @JsonIgnoreUnknownKeys annotation (#2874)
for more fine-grained control over JsonBuilder.ignoreUnknownKeys setting. Fixes #1420 Also, improve error message and path handling when an 'Unknown key' exception is thrown. Fixes #2869 Fixes #2637
1 parent 6684f67 commit aee6336

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+626
-448
lines changed

docs/basic-serialization.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ Attempts to explicitly specify its value in the serial format, even if the speci
411411
value is equal to the default one, produces the following exception.
412412

413413
```text
414-
Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 42: Encountered an unknown key 'language' at path: $.name
415-
Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.
414+
Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Encountered an unknown key 'language' at offset 42 at path: $
415+
Use 'ignoreUnknownKeys = true' in 'Json {}' builder or '@JsonIgnoreUnknownKeys' annotation to ignore unknown keys.
416416
```
417417

418418
<!--- TEST LINES_START -->

docs/json.md

+68-28
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json
1313
* [Pretty printing](#pretty-printing)
1414
* [Lenient parsing](#lenient-parsing)
1515
* [Ignoring unknown keys](#ignoring-unknown-keys)
16+
* [Ignoring unknown keys per class](#ignoring-unknown-keys-per-class)
1617
* [Alternative Json names](#alternative-json-names)
1718
* [Encoding defaults](#encoding-defaults)
1819
* [Explicit nulls](#explicit-nulls)
@@ -164,6 +165,44 @@ Project(name=kotlinx.serialization)
164165

165166
<!--- TEST -->
166167

168+
### Ignoring unknown keys per class
169+
170+
Sometimes, for cleaner and safer API, it is desirable to ignore unknown properties only for specific classes.
171+
In that case, you can use [JsonIgnoreUnknownKeys] annotation on such classes while leaving global [ignoreUnknownKeys][JsonBuilder.ignoreUnknownKeys] setting
172+
turned off:
173+
174+
```kotlin
175+
@OptIn(ExperimentalSerializationApi::class) // JsonIgnoreUnknownKeys is an experimental annotation for now
176+
@Serializable
177+
@JsonIgnoreUnknownKeys
178+
data class Outer(val a: Int, val inner: Inner)
179+
180+
@Serializable
181+
data class Inner(val x: String)
182+
183+
fun main() {
184+
// 1
185+
println(Json.decodeFromString<Outer>("""{"a":1,"inner":{"x":"value"},"unknownKey":42}"""))
186+
println()
187+
// 2
188+
println(Json.decodeFromString<Outer>("""{"a":1,"inner":{"x":"value","unknownKey":"unknownValue"}}"""))
189+
}
190+
```
191+
192+
> You can get the full code [here](../guide/example/example-json-04.kt).
193+
194+
Line (1) decodes successfully despite "unknownKey" in `Outer`, because annotation is present on the class.
195+
However, line (2) throws `SerializationException` because there is no "unknownKey" property in `Inner`:
196+
197+
```text
198+
Outer(a=1, inner=Inner(x=value))
199+
200+
Exception in thread "main" kotlinx.serialization.json.internal.JsonDecodingException: Encountered an unknown key 'unknownKey' at offset 29 at path: $.inner
201+
Use 'ignoreUnknownKeys = true' in 'Json {}' builder or '@JsonIgnoreUnknownKeys' annotation to ignore unknown keys.
202+
```
203+
204+
<!--- TEST LINES_START-->
205+
167206
### Alternative Json names
168207

169208
It's not a rare case when JSON fields are renamed due to a schema version change.
@@ -184,7 +223,7 @@ fun main() {
184223
}
185224
```
186225

187-
> You can get the full code [here](../guide/example/example-json-04.kt).
226+
> You can get the full code [here](../guide/example/example-json-05.kt).
188227
189228
As you can see, both `name` and `title` Json fields correspond to `name` property:
190229

@@ -222,7 +261,7 @@ fun main() {
222261
}
223262
```
224263

225-
> You can get the full code [here](../guide/example/example-json-05.kt).
264+
> You can get the full code [here](../guide/example/example-json-06.kt).
226265
227266
It produces the following output which encodes all the property values including the default ones:
228267

@@ -261,7 +300,7 @@ fun main() {
261300
}
262301
```
263302

264-
> You can get the full code [here](../guide/example/example-json-06.kt).
303+
> You can get the full code [here](../guide/example/example-json-07.kt).
265304
266305
As you can see, `version`, `website` and `description` fields are not present in output JSON on the first line.
267306
After decoding, the missing nullable property `website` without a default values has received a `null` value,
@@ -319,7 +358,7 @@ fun main() {
319358
}
320359
```
321360

322-
> You can get the full code [here](../guide/example/example-json-07.kt).
361+
> You can get the full code [here](../guide/example/example-json-08.kt).
323362
324363
The invalid `null` value for the `language` property was coerced into the default value:
325364

@@ -348,7 +387,7 @@ fun main() {
348387
}
349388
```
350389

351-
> You can get the full code [here](../guide/example/example-json-08.kt).
390+
> You can get the full code [here](../guide/example/example-json-09.kt).
352391
353392
Despite that we do not have `Color.pink` and `Color.purple` colors, `decodeFromString` function returns successfully:
354393

@@ -384,7 +423,7 @@ fun main() {
384423
}
385424
```
386425

387-
> You can get the full code [here](../guide/example/example-json-09.kt).
426+
> You can get the full code [here](../guide/example/example-json-10.kt).
388427
389428
The map with structured keys gets represented as JSON array with the following items: `[key1, value1, key2, value2,...]`.
390429

@@ -415,7 +454,7 @@ fun main() {
415454
}
416455
```
417456

418-
> You can get the full code [here](../guide/example/example-json-10.kt).
457+
> You can get the full code [here](../guide/example/example-json-11.kt).
419458
420459
This example produces the following non-stardard JSON output, yet it is a widely used encoding for
421460
special values in JVM world:
@@ -449,7 +488,7 @@ fun main() {
449488
}
450489
```
451490

452-
> You can get the full code [here](../guide/example/example-json-11.kt).
491+
> You can get the full code [here](../guide/example/example-json-12.kt).
453492
454493
In combination with an explicitly specified [SerialName] of the class it provides full
455494
control over the resulting JSON object:
@@ -506,7 +545,7 @@ fun main() {
506545
}
507546
```
508547

509-
> You can get the full code [here](../guide/example/example-json-12.kt).
548+
> You can get the full code [here](../guide/example/example-json-13.kt).
510549
511550
As you can see, discriminator from the `Base` class is used:
512551

@@ -543,7 +582,7 @@ fun main() {
543582
}
544583
```
545584

546-
> You can get the full code [here](../guide/example/example-json-13.kt).
585+
> You can get the full code [here](../guide/example/example-json-14.kt).
547586
548587
Note that it would be impossible to deserialize this output back with kotlinx.serialization.
549588

@@ -579,7 +618,7 @@ fun main() {
579618
}
580619
```
581620

582-
> You can get the full code [here](../guide/example/example-json-14.kt).
621+
> You can get the full code [here](../guide/example/example-json-15.kt).
583622
584623
It affects serial names as well as alternative names specified with [JsonNames] annotation, so both values are successfully decoded:
585624

@@ -612,7 +651,7 @@ fun main() {
612651
}
613652
```
614653

615-
> You can get the full code [here](../guide/example/example-json-15.kt).
654+
> You can get the full code [here](../guide/example/example-json-16.kt).
616655
617656
As you can see, both serialization and deserialization work as if all serial names are transformed from camel case to snake case:
618657

@@ -710,7 +749,7 @@ fun main() {
710749
}
711750
```
712751

713-
> You can get the full code [here](../guide/example/example-json-16.kt)
752+
> You can get the full code [here](../guide/example/example-json-17.kt)
714753
715754
```text
716755
{"base64Input":"Zm9vIHN0cmluZw=="}
@@ -752,7 +791,7 @@ fun main() {
752791
}
753792
```
754793

755-
> You can get the full code [here](../guide/example/example-json-17.kt).
794+
> You can get the full code [here](../guide/example/example-json-18.kt).
756795
757796
A `JsonElement` prints itself as a valid JSON:
758797

@@ -795,7 +834,7 @@ fun main() {
795834
}
796835
```
797836

798-
> You can get the full code [here](../guide/example/example-json-18.kt).
837+
> You can get the full code [here](../guide/example/example-json-19.kt).
799838
800839
The above example sums `votes` in all objects in the `forks` array, ignoring the objects that have no `votes`:
801840

@@ -835,7 +874,7 @@ fun main() {
835874
}
836875
```
837876

838-
> You can get the full code [here](../guide/example/example-json-19.kt).
877+
> You can get the full code [here](../guide/example/example-json-20.kt).
839878
840879
As a result, you get a proper JSON string:
841880

@@ -864,7 +903,7 @@ fun main() {
864903
}
865904
```
866905

867-
> You can get the full code [here](../guide/example/example-json-20.kt).
906+
> You can get the full code [here](../guide/example/example-json-21.kt).
868907
869908
The result is exactly what you would expect:
870909

@@ -910,7 +949,7 @@ fun main() {
910949
}
911950
```
912951

913-
> You can get the full code [here](../guide/example/example-json-21.kt).
952+
> You can get the full code [here](../guide/example/example-json-22.kt).
914953
915954
Even though `pi` was defined as a number with 30 decimal places, the resulting JSON does not reflect this.
916955
The [Double] value is truncated to 15 decimal places, and the String is wrapped in quotes - which is not a JSON number.
@@ -951,7 +990,7 @@ fun main() {
951990
}
952991
```
953992

954-
> You can get the full code [here](../guide/example/example-json-22.kt).
993+
> You can get the full code [here](../guide/example/example-json-23.kt).
955994
956995
`pi_literal` now accurately matches the value defined.
957996

@@ -991,7 +1030,7 @@ fun main() {
9911030
}
9921031
```
9931032

994-
> You can get the full code [here](../guide/example/example-json-23.kt).
1033+
> You can get the full code [here](../guide/example/example-json-24.kt).
9951034
9961035
The exact value of `pi` is decoded, with all 30 decimal places of precision that were in the source JSON.
9971036

@@ -1014,7 +1053,7 @@ fun main() {
10141053
}
10151054
```
10161055

1017-
> You can get the full code [here](../guide/example/example-json-24.kt).
1056+
> You can get the full code [here](../guide/example/example-json-25.kt).
10181057
10191058
```text
10201059
Exception in thread "main" kotlinx.serialization.json.internal.JsonEncodingException: Creating a literal unquoted value of 'null' is forbidden. If you want to create JSON null literal, use JsonNull object, otherwise, use JsonPrimitive
@@ -1090,7 +1129,7 @@ fun main() {
10901129
}
10911130
```
10921131

1093-
> You can get the full code [here](../guide/example/example-json-25.kt).
1132+
> You can get the full code [here](../guide/example/example-json-26.kt).
10941133
10951134
The output shows that both cases are correctly deserialized into a Kotlin [List].
10961135

@@ -1142,7 +1181,7 @@ fun main() {
11421181
}
11431182
```
11441183

1145-
> You can get the full code [here](../guide/example/example-json-26.kt).
1184+
> You can get the full code [here](../guide/example/example-json-27.kt).
11461185
11471186
You end up with a single JSON object, not an array with one element:
11481187

@@ -1187,7 +1226,7 @@ fun main() {
11871226
}
11881227
```
11891228

1190-
> You can get the full code [here](../guide/example/example-json-27.kt).
1229+
> You can get the full code [here](../guide/example/example-json-28.kt).
11911230
11921231
See the effect of the custom serializer:
11931232

@@ -1260,7 +1299,7 @@ fun main() {
12601299
}
12611300
```
12621301

1263-
> You can get the full code [here](../guide/example/example-json-28.kt).
1302+
> You can get the full code [here](../guide/example/example-json-29.kt).
12641303
12651304
No class discriminator is added in the JSON output:
12661305

@@ -1312,7 +1351,7 @@ fun main() {
13121351
}
13131352
```
13141353

1315-
> You can get the full code [here](../guide/example/example-json-29.kt).
1354+
> You can get the full code [here](../guide/example/example-json-30.kt).
13161355
13171356
`BasicProject` will be printed to the output:
13181357

@@ -1406,7 +1445,7 @@ fun main() {
14061445
}
14071446
```
14081447

1409-
> You can get the full code [here](../guide/example/example-json-30.kt).
1448+
> You can get the full code [here](../guide/example/example-json-31.kt).
14101449
14111450
This gives you fine-grained control on the representation of the `Response` class in the JSON output:
14121451

@@ -1471,7 +1510,7 @@ fun main() {
14711510
}
14721511
```
14731512

1474-
> You can get the full code [here](../guide/example/example-json-31.kt).
1513+
> You can get the full code [here](../guide/example/example-json-32.kt).
14751514
14761515
```text
14771516
UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})
@@ -1517,6 +1556,7 @@ The next chapter covers [Alternative and custom formats (experimental)](formats.
15171556
[JsonBuilder.prettyPrint]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/pretty-print.html
15181557
[JsonBuilder.isLenient]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/is-lenient.html
15191558
[JsonBuilder.ignoreUnknownKeys]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/ignore-unknown-keys.html
1559+
[JsonIgnoreUnknownKeys]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-ignore-unknown-keys/index.html
15201560
[JsonNames]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-names/index.html
15211561
[JsonBuilder.useAlternativeNames]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/use-alternative-names.html
15221562
[JsonBuilder.encodeDefaults]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/encode-defaults.html

docs/serialization-guide.md

+1
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Once the project is set up, we can start serializing some classes.
114114
* <a name='pretty-printing'></a>[Pretty printing](json.md#pretty-printing)
115115
* <a name='lenient-parsing'></a>[Lenient parsing](json.md#lenient-parsing)
116116
* <a name='ignoring-unknown-keys'></a>[Ignoring unknown keys](json.md#ignoring-unknown-keys)
117+
* <a name='ignoring-unknown-keys-per-class'></a>[Ignoring unknown keys per class](json.md#ignoring-unknown-keys-per-class)
117118
* <a name='alternative-json-names'></a>[Alternative Json names](json.md#alternative-json-names)
118119
* <a name='encoding-defaults'></a>[Encoding defaults](json.md#encoding-defaults)
119120
* <a name='explicit-nulls'></a>[Explicit nulls](json.md#explicit-nulls)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2017-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.json
6+
7+
import kotlinx.serialization.Serializable
8+
import kotlinx.serialization.test.checkSerializationException
9+
import kotlin.test.Test
10+
import kotlin.test.assertContains
11+
import kotlin.test.assertContentEquals
12+
import kotlin.test.assertEquals
13+
14+
class JsonIgnoreKeysTest : JsonTestBase() {
15+
val ignoresKeys = Json(default) { ignoreUnknownKeys = true }
16+
17+
@Serializable
18+
class Outer(val a: Int, val inner: Inner)
19+
20+
@Serializable
21+
@JsonIgnoreUnknownKeys
22+
class Inner(val x: String)
23+
24+
@Test
25+
fun testIgnoresKeyWhenGlobalSettingNotSet() = parametrizedTest { mode ->
26+
val jsonString = """{"a":1,"inner":{"x":"value","unknownKey":"unknownValue"}}"""
27+
val result = default.decodeFromString<Outer>(jsonString, mode)
28+
assertEquals(1, result.a)
29+
assertEquals("value", result.inner.x)
30+
}
31+
32+
@Test
33+
fun testThrowsWithoutAnnotationWhenGlobalSettingNotSet() = parametrizedTest { mode ->
34+
val jsonString = """{"a":1,"inner":{"x":"value","unknownKey":"unknownValue"}, "b":2}"""
35+
checkSerializationException({
36+
default.decodeFromString<Outer>(jsonString, mode)
37+
}) { msg ->
38+
assertContains(
39+
msg,
40+
if (mode == JsonTestingMode.TREE) "Encountered an unknown key 'b' at element: \$\n"
41+
else "Encountered an unknown key 'b' at offset 59 at path: \$\n"
42+
)
43+
}
44+
}
45+
46+
@Test
47+
fun testIgnoresBothKeysWithGlobalSetting() = parametrizedTest { mode ->
48+
val jsonString = """{"a":1,"inner":{"x":"value","unknownKey":"unknownValue"}, "b":2}"""
49+
val result = ignoresKeys.decodeFromString<Outer>(jsonString, mode)
50+
assertEquals(1, result.a)
51+
assertEquals("value", result.inner.x)
52+
}
53+
}

formats/json/api/kotlinx-serialization-json.api

+7
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@ public final class kotlinx/serialization/json/JsonEncoder$DefaultImpls {
262262
public static fun shouldEncodeElementDefault (Lkotlinx/serialization/json/JsonEncoder;Lkotlinx/serialization/descriptors/SerialDescriptor;I)Z
263263
}
264264

265+
public abstract interface annotation class kotlinx/serialization/json/JsonIgnoreUnknownKeys : java/lang/annotation/Annotation {
266+
}
267+
268+
public synthetic class kotlinx/serialization/json/JsonIgnoreUnknownKeys$Impl : kotlinx/serialization/json/JsonIgnoreUnknownKeys {
269+
public fun <init> ()V
270+
}
271+
265272
public final class kotlinx/serialization/json/JsonKt {
266273
public static final fun Json (Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/json/Json;
267274
public static synthetic fun Json$default (Lkotlinx/serialization/json/Json;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/json/Json;

0 commit comments

Comments
 (0)