Skip to content

Commit f525f1a

Browse files
Add 'how to' section for ByteArray<>Base64 serializer (#2644)
Closes #1633 Signed-off-by: George Papadopoulos <[email protected]>
1 parent 28a5f74 commit f525f1a

18 files changed

+381
-226
lines changed

docs/json.md

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ In this chapter, we'll walk through features of [JSON](https://www.json.org/json
2323
* [Class discriminator output mode](#class-discriminator-output-mode)
2424
* [Decoding enums in a case-insensitive manner](#decoding-enums-in-a-case-insensitive-manner)
2525
* [Global naming strategy](#global-naming-strategy)
26+
* [Base64](#base64)
2627
* [Json elements](#json-elements)
2728
* [Parsing to Json element](#parsing-to-json-element)
2829
* [Types of Json elements](#types-of-json-elements)
@@ -591,6 +592,94 @@ Therefore, one should carefully weigh the pros and cons before considering addin
591592

592593
<!--- TEST -->
593594

595+
### Base64
596+
597+
To encode and decode Base64 formats, we will need to manually write a serializer. Here, we will use a default
598+
implementation of Kotlin's Base64 encoder. Note that some serializers use different RFCs for Base64 encoding by default.
599+
For example, Jackson uses a variant of [Base64 Mime](https://datatracker.ietf.org/doc/html/rfc2045). The same result in
600+
kotlinx.serialization can be achieved with Base64.Mime encoder.
601+
[Kotlin's documentation for Base64](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io.encoding/-base64/) lists
602+
other available encoders.
603+
604+
```kotlin
605+
import kotlinx.serialization.encoding.Encoder
606+
import kotlinx.serialization.encoding.Decoder
607+
import kotlinx.serialization.descriptors.*
608+
import kotlin.io.encoding.*
609+
610+
@OptIn(ExperimentalEncodingApi::class)
611+
object ByteArrayAsBase64Serializer : KSerializer<ByteArray> {
612+
private val base64 = Base64.Default
613+
614+
override val descriptor: SerialDescriptor
615+
get() = PrimitiveSerialDescriptor(
616+
"ByteArrayAsBase64Serializer",
617+
PrimitiveKind.STRING
618+
)
619+
620+
override fun serialize(encoder: Encoder, value: ByteArray) {
621+
val base64Encoded = base64.encode(value)
622+
encoder.encodeString(base64Encoded)
623+
}
624+
625+
override fun deserialize(decoder: Decoder): ByteArray {
626+
val base64Decoded = decoder.decodeString()
627+
return base64.decode(base64Decoded)
628+
}
629+
}
630+
```
631+
632+
For more details on how to create your own custom serializer, you can
633+
see [custom serializers](serializers.md#custom-serializers).
634+
635+
Then we can use it like this:
636+
637+
```kotlin
638+
@Serializable
639+
data class Value(
640+
@Serializable(with = ByteArrayAsBase64Serializer::class)
641+
val base64Input: ByteArray
642+
) {
643+
override fun equals(other: Any?): Boolean {
644+
if (this === other) return true
645+
if (javaClass != other?.javaClass) return false
646+
other as Value
647+
return base64Input.contentEquals(other.base64Input)
648+
}
649+
650+
override fun hashCode(): Int {
651+
return base64Input.contentHashCode()
652+
}
653+
}
654+
655+
fun main() {
656+
val string = "foo string"
657+
val value = Value(string.toByteArray())
658+
val encoded = Json.encodeToString(value)
659+
println(encoded)
660+
val decoded = Json.decodeFromString<Value>(encoded)
661+
println(decoded.base64Input.decodeToString())
662+
}
663+
```
664+
665+
> You can get the full code [here](../guide/example/example-json-15.kt)
666+
667+
```text
668+
{"base64Input":"Zm9vIHN0cmluZw=="}
669+
foo string
670+
```
671+
672+
Notice the serializer we wrote is not dependent on `Json` format, therefore, it can be used in any format.
673+
674+
For projects that use this serializer in many places, to avoid specifying the serializer every time, it is possible
675+
to [specify a serializer globally using typealias](serializers.md#specifying-serializer-globally-using-typealias).
676+
For example:
677+
````kotlin
678+
typealias Base64ByteArray = @Serializable(ByteArrayAsBase64Serializer::class) ByteArray
679+
````
680+
681+
<!--- TEST -->
682+
594683
## Json elements
595684

596685
Aside from direct conversions between strings and JSON objects, Kotlin serialization offers APIs that allow
@@ -615,7 +704,7 @@ fun main() {
615704
}
616705
```
617706

618-
> You can get the full code [here](../guide/example/example-json-15.kt).
707+
> You can get the full code [here](../guide/example/example-json-16.kt).
619708
620709
A `JsonElement` prints itself as a valid JSON:
621710

@@ -658,7 +747,7 @@ fun main() {
658747
}
659748
```
660749

661-
> You can get the full code [here](../guide/example/example-json-16.kt).
750+
> You can get the full code [here](../guide/example/example-json-17.kt).
662751
663752
The above example sums `votes` in all objects in the `forks` array, ignoring the objects that have no `votes`:
664753

@@ -698,7 +787,7 @@ fun main() {
698787
}
699788
```
700789

701-
> You can get the full code [here](../guide/example/example-json-17.kt).
790+
> You can get the full code [here](../guide/example/example-json-18.kt).
702791
703792
As a result, you get a proper JSON string:
704793

@@ -727,7 +816,7 @@ fun main() {
727816
}
728817
```
729818

730-
> You can get the full code [here](../guide/example/example-json-18.kt).
819+
> You can get the full code [here](../guide/example/example-json-19.kt).
731820
732821
The result is exactly what you would expect:
733822

@@ -773,7 +862,7 @@ fun main() {
773862
}
774863
```
775864

776-
> You can get the full code [here](../guide/example/example-json-19.kt).
865+
> You can get the full code [here](../guide/example/example-json-20.kt).
777866
778867
Even though `pi` was defined as a number with 30 decimal places, the resulting JSON does not reflect this.
779868
The [Double] value is truncated to 15 decimal places, and the String is wrapped in quotes - which is not a JSON number.
@@ -813,7 +902,7 @@ fun main() {
813902
}
814903
```
815904

816-
> You can get the full code [here](../guide/example/example-json-20.kt).
905+
> You can get the full code [here](../guide/example/example-json-21.kt).
817906
818907
`pi_literal` now accurately matches the value defined.
819908

@@ -853,7 +942,7 @@ fun main() {
853942
}
854943
```
855944

856-
> You can get the full code [here](../guide/example/example-json-21.kt).
945+
> You can get the full code [here](../guide/example/example-json-22.kt).
857946
858947
The exact value of `pi` is decoded, with all 30 decimal places of precision that were in the source JSON.
859948

@@ -875,7 +964,7 @@ fun main() {
875964
}
876965
```
877966

878-
> You can get the full code [here](../guide/example/example-json-22.kt).
967+
> You can get the full code [here](../guide/example/example-json-23.kt).
879968
880969
```text
881970
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
@@ -951,7 +1040,7 @@ fun main() {
9511040
}
9521041
```
9531042

954-
> You can get the full code [here](../guide/example/example-json-23.kt).
1043+
> You can get the full code [here](../guide/example/example-json-24.kt).
9551044
9561045
The output shows that both cases are correctly deserialized into a Kotlin [List].
9571046

@@ -1003,7 +1092,7 @@ fun main() {
10031092
}
10041093
```
10051094

1006-
> You can get the full code [here](../guide/example/example-json-24.kt).
1095+
> You can get the full code [here](../guide/example/example-json-25.kt).
10071096
10081097
You end up with a single JSON object, not an array with one element:
10091098

@@ -1048,7 +1137,7 @@ fun main() {
10481137
}
10491138
```
10501139

1051-
> You can get the full code [here](../guide/example/example-json-25.kt).
1140+
> You can get the full code [here](../guide/example/example-json-26.kt).
10521141
10531142
See the effect of the custom serializer:
10541143

@@ -1121,7 +1210,7 @@ fun main() {
11211210
}
11221211
```
11231212

1124-
> You can get the full code [here](../guide/example/example-json-26.kt).
1213+
> You can get the full code [here](../guide/example/example-json-27.kt).
11251214
11261215
No class discriminator is added in the JSON output:
11271216

@@ -1217,7 +1306,7 @@ fun main() {
12171306
}
12181307
```
12191308

1220-
> You can get the full code [here](../guide/example/example-json-27.kt).
1309+
> You can get the full code [here](../guide/example/example-json-28.kt).
12211310
12221311
This gives you fine-grained control on the representation of the `Response` class in the JSON output:
12231312

@@ -1282,7 +1371,7 @@ fun main() {
12821371
}
12831372
```
12841373

1285-
> You can get the full code [here](../guide/example/example-json-28.kt).
1374+
> You can get the full code [here](../guide/example/example-json-29.kt).
12861375
12871376
```text
12881377
UnknownProject(name=example, details={"type":"unknown","maintainer":"Unknown","license":"Apache 2.0"})

docs/serialization-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ Once the project is set up, we can start serializing some classes.
123123
* <a name='class-discriminator-output-mode'></a>[Class discriminator output mode](json.md#class-discriminator-output-mode)
124124
* <a name='decoding-enums-in-a-case-insensitive-manner'></a>[Decoding enums in a case-insensitive manner](json.md#decoding-enums-in-a-case-insensitive-manner)
125125
* <a name='global-naming-strategy'></a>[Global naming strategy](json.md#global-naming-strategy)
126+
* <a name='base64'></a>[Base64](json.md#base64)
126127
* <a name='json-elements'></a>[Json elements](json.md#json-elements)
127128
* <a name='parsing-to-json-element'></a>[Parsing to Json element](json.md#parsing-to-json-element)
128129
* <a name='types-of-json-elements'></a>[Types of Json elements](json.md#types-of-json-elements)

guide/example/example-json-15.kt

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

7+
import kotlinx.serialization.encoding.Encoder
8+
import kotlinx.serialization.encoding.Decoder
9+
import kotlinx.serialization.descriptors.*
10+
import kotlin.io.encoding.*
11+
12+
@OptIn(ExperimentalEncodingApi::class)
13+
object ByteArrayAsBase64Serializer : KSerializer<ByteArray> {
14+
private val base64 = Base64.Default
15+
16+
override val descriptor: SerialDescriptor
17+
get() = PrimitiveSerialDescriptor(
18+
"ByteArrayAsBase64Serializer",
19+
PrimitiveKind.STRING
20+
)
21+
22+
override fun serialize(encoder: Encoder, value: ByteArray) {
23+
val base64Encoded = base64.encode(value)
24+
encoder.encodeString(base64Encoded)
25+
}
26+
27+
override fun deserialize(decoder: Decoder): ByteArray {
28+
val base64Decoded = decoder.decodeString()
29+
return base64.decode(base64Decoded)
30+
}
31+
}
32+
33+
@Serializable
34+
data class Value(
35+
@Serializable(with = ByteArrayAsBase64Serializer::class)
36+
val base64Input: ByteArray
37+
) {
38+
override fun equals(other: Any?): Boolean {
39+
if (this === other) return true
40+
if (javaClass != other?.javaClass) return false
41+
other as Value
42+
return base64Input.contentEquals(other.base64Input)
43+
}
44+
45+
override fun hashCode(): Int {
46+
return base64Input.contentHashCode()
47+
}
48+
}
49+
750
fun main() {
8-
val element = Json.parseToJsonElement("""
9-
{"name":"kotlinx.serialization","language":"Kotlin"}
10-
""")
11-
println(element)
51+
val string = "foo string"
52+
val value = Value(string.toByteArray())
53+
val encoded = Json.encodeToString(value)
54+
println(encoded)
55+
val decoded = Json.decodeFromString<Value>(encoded)
56+
println(decoded.base64Input.decodeToString())
1257
}

guide/example/example-json-16.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-17.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-18.kt

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,20 @@ package example.exampleJson18
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-19.kt

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,14 @@ package example.exampleJson19
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
}

0 commit comments

Comments
 (0)