Skip to content

Commit 7a35a2d

Browse files
Add documentation for JsonNamingStrategy to the Json guide (#2305)
to increase adoption. Fixes #2206 Co-authored-by: Vsevolod Tolstopyatov <qwwdfsad@gmail.com>
1 parent 06aabd2 commit 7a35a2d

18 files changed

+299
-226
lines changed

docs/json.md

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json
2020
* [Allowing structured map keys](#allowing-structured-map-keys)
2121
* [Allowing special floating-point values](#allowing-special-floating-point-values)
2222
* [Class discriminator for polymorphism](#class-discriminator-for-polymorphism)
23+
* [Global naming strategy](#global-naming-strategy)
2324
* [Json elements](#json-elements)
2425
* [Parsing to Json element](#parsing-to-json-element)
2526
* [Types of Json elements](#types-of-json-elements)
@@ -468,6 +469,53 @@ As you can see, discriminator from the `Base` class is used:
468469

469470
<!--- TEST -->
470471

472+
### Global naming strategy
473+
474+
If properties' names in Json input are different from Kotlin ones, it is recommended to specify the name
475+
for each property explicitly using [`@SerialName` annotation](basic-serialization.md#serial-field-names).
476+
However, there are certain situations where transformation should be applied to every serial name — such as migration
477+
from other frameworks or legacy codebase. For that cases, it is possible to specify a [namingStrategy][JsonBuilder.namingStrategy]
478+
for a [Json] instance. `kotlinx.serialization` provides one strategy implementation out of the box, the [JsonNamingStrategy.SnakeCase](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-naming-strategy/-builtins/-snake-case.html):
479+
480+
```kotlin
481+
@Serializable
482+
data class Project(val projectName: String, val projectOwner: String)
483+
484+
val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }
485+
486+
fun main() {
487+
val project = format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
488+
println(format.encodeToString(project.copy(projectName = "kotlinx.serialization")))
489+
}
490+
```
491+
492+
> You can get the full code [here](../guide/example/example-json-12.kt).
493+
494+
As you can see, both serialization and deserialization work as if all serial names are transformed from camel case to snake case:
495+
496+
```text
497+
{"project_name":"kotlinx.serialization","project_owner":"Kotlin"}
498+
```
499+
500+
There are some caveats one should remember while dealing with a [JsonNamingStrategy]:
501+
502+
* Due to the nature of the `kotlinx.serialization` framework, naming strategy transformation is applied to all properties regardless
503+
of whether their serial name was taken from the property name or provided by [SerialName] annotation.
504+
Effectively, it means one cannot avoid transformation by explicitly specifying the serial name. To be able to deserialize
505+
non-transformed names, [JsonNames] annotation can be used instead.
506+
507+
* Collision of the transformed name with any other (transformed) properties serial names or any alternative names
508+
specified with [JsonNames] will lead to a deserialization exception.
509+
510+
* Global naming strategies are very implicit: by looking only at the definition of the class,
511+
it is impossible to determine which names it will have in the serialized form.
512+
As a consequence, naming strategies are not friendly to actions like Find Usages/Rename in IDE, full-text search by grep, etc.
513+
For them, the original name and the transformed are two different things;
514+
changing one without the other may introduce bugs in many unexpected ways and lead to greater maintenance efforts for code with global naming strategies.
515+
516+
Therefore, one should carefully weigh the pros and cons before considering adding global naming strategies to an application.
517+
518+
<!--- TEST -->
471519

472520
## Json elements
473521

@@ -493,7 +541,7 @@ fun main() {
493541
}
494542
```
495543

496-
> You can get the full code [here](../guide/example/example-json-12.kt).
544+
> You can get the full code [here](../guide/example/example-json-13.kt).
497545
498546
A `JsonElement` prints itself as a valid JSON:
499547

@@ -536,7 +584,7 @@ fun main() {
536584
}
537585
```
538586

539-
> You can get the full code [here](../guide/example/example-json-13.kt).
587+
> You can get the full code [here](../guide/example/example-json-14.kt).
540588
541589
The above example sums `votes` in all objects in the `forks` array, ignoring the objects that have no `votes`:
542590

@@ -576,7 +624,7 @@ fun main() {
576624
}
577625
```
578626

579-
> You can get the full code [here](../guide/example/example-json-14.kt).
627+
> You can get the full code [here](../guide/example/example-json-15.kt).
580628
581629
As a result, you get a proper JSON string:
582630

@@ -605,7 +653,7 @@ fun main() {
605653
}
606654
```
607655

608-
> You can get the full code [here](../guide/example/example-json-15.kt).
656+
> You can get the full code [here](../guide/example/example-json-16.kt).
609657
610658
The result is exactly what you would expect:
611659

@@ -651,7 +699,7 @@ fun main() {
651699
}
652700
```
653701

654-
> You can get the full code [here](../guide/example/example-json-16.kt).
702+
> You can get the full code [here](../guide/example/example-json-17.kt).
655703
656704
Even though `pi` was defined as a number with 30 decimal places, the resulting JSON does not reflect this.
657705
The [Double] value is truncated to 15 decimal places, and the String is wrapped in quotes - which is not a JSON number.
@@ -691,7 +739,7 @@ fun main() {
691739
}
692740
```
693741

694-
> You can get the full code [here](../guide/example/example-json-17.kt).
742+
> You can get the full code [here](../guide/example/example-json-18.kt).
695743
696744
`pi_literal` now accurately matches the value defined.
697745

@@ -731,7 +779,7 @@ fun main() {
731779
}
732780
```
733781

734-
> You can get the full code [here](../guide/example/example-json-18.kt).
782+
> You can get the full code [here](../guide/example/example-json-19.kt).
735783
736784
The exact value of `pi` is decoded, with all 30 decimal places of precision that were in the source JSON.
737785

@@ -753,7 +801,7 @@ fun main() {
753801
}
754802
```
755803

756-
> You can get the full code [here](../guide/example/example-json-19.kt).
804+
> You can get the full code [here](../guide/example/example-json-20.kt).
757805
758806
```text
759807
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
@@ -829,7 +877,7 @@ fun main() {
829877
}
830878
```
831879

832-
> You can get the full code [here](../guide/example/example-json-20.kt).
880+
> You can get the full code [here](../guide/example/example-json-21.kt).
833881
834882
The output shows that both cases are correctly deserialized into a Kotlin [List].
835883

@@ -881,7 +929,7 @@ fun main() {
881929
}
882930
```
883931

884-
> You can get the full code [here](../guide/example/example-json-21.kt).
932+
> You can get the full code [here](../guide/example/example-json-22.kt).
885933
886934
You end up with a single JSON object, not an array with one element:
887935

@@ -926,7 +974,7 @@ fun main() {
926974
}
927975
```
928976

929-
> You can get the full code [here](../guide/example/example-json-22.kt).
977+
> You can get the full code [here](../guide/example/example-json-23.kt).
930978
931979
See the effect of the custom serializer:
932980

@@ -999,7 +1047,7 @@ fun main() {
9991047
}
10001048
```
10011049

1002-
> You can get the full code [here](../guide/example/example-json-23.kt).
1050+
> You can get the full code [here](../guide/example/example-json-24.kt).
10031051
10041052
No class discriminator is added in the JSON output:
10051053

@@ -1095,7 +1143,7 @@ fun main() {
10951143
}
10961144
```
10971145

1098-
> You can get the full code [here](../guide/example/example-json-24.kt).
1146+
> You can get the full code [here](../guide/example/example-json-25.kt).
10991147
11001148
This gives you fine-grained control on the representation of the `Response` class in the JSON output:
11011149

@@ -1160,7 +1208,7 @@ fun main() {
11601208
}
11611209
```
11621210

1163-
> You can get the full code [here](../guide/example/example-json-25.kt).
1211+
> You can get the full code [here](../guide/example/example-json-26.kt).
11641212
11651213
```text
11661214
UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})
@@ -1214,6 +1262,8 @@ The next chapter covers [Alternative and custom formats (experimental)](formats.
12141262
[JsonBuilder.allowSpecialFloatingPointValues]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/allow-special-floating-point-values.html
12151263
[JsonBuilder.classDiscriminator]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/class-discriminator.html
12161264
[JsonClassDiscriminator]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-class-discriminator/index.html
1265+
[JsonBuilder.namingStrategy]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-builder/naming-strategy.html
1266+
[JsonNamingStrategy]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-naming-strategy/index.html
12171267
[JsonElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-element/index.html
12181268
[Json.parseToJsonElement]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json/parse-to-json-element.html
12191269
[JsonPrimitive]: https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-json/kotlinx.serialization.json/-json-primitive/index.html

docs/serialization-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ Once the project is set up, we can start serializing some classes.
119119
* <a name='allowing-structured-map-keys'></a>[Allowing structured map keys](json.md#allowing-structured-map-keys)
120120
* <a name='allowing-special-floating-point-values'></a>[Allowing special floating-point values](json.md#allowing-special-floating-point-values)
121121
* <a name='class-discriminator-for-polymorphism'></a>[Class discriminator for polymorphism](json.md#class-discriminator-for-polymorphism)
122+
* <a name='global-naming-strategy'></a>[Global naming strategy](json.md#global-naming-strategy)
122123
* <a name='json-elements'></a>[Json elements](json.md#json-elements)
123124
* <a name='parsing-to-json-element'></a>[Parsing to Json element](json.md#parsing-to-json-element)
124125
* <a name='types-of-json-elements'></a>[Types of Json elements](json.md#types-of-json-elements)

guide/example/example-json-12.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ package example.exampleJson12
44
import kotlinx.serialization.*
55
import kotlinx.serialization.json.*
66

7+
@Serializable
8+
data class Project(val projectName: String, val projectOwner: String)
9+
10+
val format = Json { namingStrategy = JsonNamingStrategy.SnakeCase }
11+
712
fun main() {
8-
val element = Json.parseToJsonElement("""
9-
{"name":"kotlinx.serialization","language":"Kotlin"}
10-
""")
11-
println(element)
13+
val project = format.decodeFromString<Project>("""{"project_name":"kotlinx.coroutines", "project_owner":"Kotlin"}""")
14+
println(format.encodeToString(project.copy(projectName = "kotlinx.serialization")))
1215
}

guide/example/example-json-13.kt

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,7 @@ import kotlinx.serialization.json.*
66

77
fun main() {
88
val element = Json.parseToJsonElement("""
9-
{
10-
"name": "kotlinx.serialization",
11-
"forks": [{"votes": 42}, {"votes": 9000}, {}]
12-
}
9+
{"name":"kotlinx.serialization","language":"Kotlin"}
1310
""")
14-
val sum = element
15-
.jsonObject["forks"]!!
16-
.jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 }
17-
println(sum)
11+
println(element)
1812
}

guide/example/example-json-14.kt

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,14 @@ import kotlinx.serialization.*
55
import kotlinx.serialization.json.*
66

77
fun main() {
8-
val element = buildJsonObject {
9-
put("name", "kotlinx.serialization")
10-
putJsonObject("owner") {
11-
put("name", "kotlin")
8+
val element = Json.parseToJsonElement("""
9+
{
10+
"name": "kotlinx.serialization",
11+
"forks": [{"votes": 42}, {"votes": 9000}, {}]
1212
}
13-
putJsonArray("forks") {
14-
addJsonObject {
15-
put("votes", 42)
16-
}
17-
addJsonObject {
18-
put("votes", 9000)
19-
}
20-
}
21-
}
22-
println(element)
13+
""")
14+
val sum = element
15+
.jsonObject["forks"]!!
16+
.jsonArray.sumOf { it.jsonObject["votes"]?.jsonPrimitive?.int ?: 0 }
17+
println(sum)
2318
}

guide/example/example-json-15.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ package example.exampleJson15
44
import kotlinx.serialization.*
55
import kotlinx.serialization.json.*
66

7-
@Serializable
8-
data class Project(val name: String, val language: String)
9-
107
fun main() {
118
val element = buildJsonObject {
129
put("name", "kotlinx.serialization")
13-
put("language", "Kotlin")
10+
putJsonObject("owner") {
11+
put("name", "kotlin")
12+
}
13+
putJsonArray("forks") {
14+
addJsonObject {
15+
put("votes", 42)
16+
}
17+
addJsonObject {
18+
put("votes", 9000)
19+
}
20+
}
1421
}
15-
val data = Json.decodeFromJsonElement<Project>(element)
16-
println(data)
22+
println(element)
1723
}

guide/example/example-json-16.kt

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,14 @@ package example.exampleJson16
44
import kotlinx.serialization.*
55
import kotlinx.serialization.json.*
66

7-
import java.math.BigDecimal
8-
9-
val format = Json { prettyPrint = true }
7+
@Serializable
8+
data class Project(val name: String, val language: String)
109

1110
fun main() {
12-
val pi = BigDecimal("3.141592653589793238462643383279")
13-
14-
val piJsonDouble = JsonPrimitive(pi.toDouble())
15-
val piJsonString = JsonPrimitive(pi.toString())
16-
17-
val piObject = buildJsonObject {
18-
put("pi_double", piJsonDouble)
19-
put("pi_string", piJsonString)
11+
val element = buildJsonObject {
12+
put("name", "kotlinx.serialization")
13+
put("language", "Kotlin")
2014
}
21-
22-
println(format.encodeToString(piObject))
15+
val data = Json.decodeFromJsonElement<Project>(element)
16+
println(data)
2317
}

guide/example/example-json-17.kt

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,11 @@ val format = Json { prettyPrint = true }
1010

1111
fun main() {
1212
val pi = BigDecimal("3.141592653589793238462643383279")
13-
14-
// use JsonUnquotedLiteral to encode raw JSON content
15-
val piJsonLiteral = JsonUnquotedLiteral(pi.toString())
16-
13+
1714
val piJsonDouble = JsonPrimitive(pi.toDouble())
1815
val piJsonString = JsonPrimitive(pi.toString())
1916

2017
val piObject = buildJsonObject {
21-
put("pi_literal", piJsonLiteral)
2218
put("pi_double", piJsonDouble)
2319
put("pi_string", piJsonString)
2420
}

guide/example/example-json-18.kt

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,22 @@ import kotlinx.serialization.json.*
66

77
import java.math.BigDecimal
88

9+
val format = Json { prettyPrint = true }
10+
911
fun main() {
10-
val piObjectJson = """
11-
{
12-
"pi_literal": 3.141592653589793238462643383279
13-
}
14-
""".trimIndent()
15-
16-
val piObject: JsonObject = Json.decodeFromString(piObjectJson)
17-
18-
val piJsonLiteral = piObject["pi_literal"]!!.jsonPrimitive.content
19-
20-
val pi = BigDecimal(piJsonLiteral)
21-
22-
println(pi)
12+
val pi = BigDecimal("3.141592653589793238462643383279")
13+
14+
// use JsonUnquotedLiteral to encode raw JSON content
15+
val piJsonLiteral = JsonUnquotedLiteral(pi.toString())
16+
17+
val piJsonDouble = JsonPrimitive(pi.toDouble())
18+
val piJsonString = JsonPrimitive(pi.toString())
19+
20+
val piObject = buildJsonObject {
21+
put("pi_literal", piJsonLiteral)
22+
put("pi_double", piJsonDouble)
23+
put("pi_string", piJsonString)
24+
}
25+
26+
println(format.encodeToString(piObject))
2327
}

guide/example/example-json-19.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,20 @@ package example.exampleJson19
44
import kotlinx.serialization.*
55
import kotlinx.serialization.json.*
66

7+
import java.math.BigDecimal
8+
79
fun main() {
8-
// caution: creating null with JsonUnquotedLiteral will cause an exception!
9-
JsonUnquotedLiteral("null")
10+
val piObjectJson = """
11+
{
12+
"pi_literal": 3.141592653589793238462643383279
13+
}
14+
""".trimIndent()
15+
16+
val piObject: JsonObject = Json.decodeFromString(piObjectJson)
17+
18+
val piJsonLiteral = piObject["pi_literal"]!!.jsonPrimitive.content
19+
20+
val pi = BigDecimal(piJsonLiteral)
21+
22+
println(pi)
1023
}

0 commit comments

Comments
 (0)