Skip to content

Conversation

@daniCsorbaJB
Copy link

This is the fourth part of the Kotlin Serialization rewrite task.

Related YouTract ticket is here: KT-81889 [Docs] Create a Serialize classes page for kotlinx.serialization

Copy link
Member

@sandwwraith sandwwraith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.

@@ -0,0 +1,505 @@
[//]: # (title: Serialize classes)

The [`@Serializable`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serializable/) annotation in Kotlin enables the serialization of all properties in classes defined by the primary constructor.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not true about primary constructor, I've already mentioned that in one of the reviews

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, good point - changed it to properties with backing fields here as well 👍

allowing a class to be converted to and from formats such as JSON.

In Kotlin, only properties with backing fields are serialized.
Properties defined with [accessors](properties.md) or delegated properties without backing fields are excluded from serialization:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can have accessors and backing fields too: https://kotlinlang.org/docs/properties.html#backing-fields
I suggest just giving link to the definition of backing field here to avoid further confusion, as repeating "Kotlin only generates backing fields if you use the default getter or setter, or if you use field in at least one custom accessor." may be too long

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 Yeah, good suggestion. I think we can get away with it with the link + the example following it 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition you may want to write it in such a way that it doesn't imply that there is no way to serialize "other" properties, while this can be done with custom serializers (not that you should really go into those here). Maybe just replace "excluded from serialization:" with something like "excluded from plugin managed serialization" (managed seems better than generated to me here)

// Declares a property with a backing field - serialized
var name: String
) {
// Declares a property with a backing field - serialized
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is "Declares ..." our standard for technical writing? I do not remember seeing it. IMO, it is quite redundant, as we know that this line is a declaration, so we get smth like "Declaration declares a property with a backing field"

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 It's used, but I see where you are coming form, so I agree.

What do you think about these options?
My immediate thought is to use "Defines" instead (to retain the third-person structure of the comment — which is the preferred style)

// Defines a property with a backing field - serialized

Alternatively, I don't think it's an issue to deviate from the preferred style here, so we could make it even shorter for this one:

// Property with a backing field - serialized.

{kotlin-runnable="true"}

Kotlin Serialization natively supports nullable properties.
Like [other _default values_](#set-default-values-for-optional-properties), `null` values aren't encoded in the JSON output:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get the following tooltip on hover:

Image

IMO it's out of place and confusing. Also, link points to "Set default values for optional properties" which doesn't mention dropping of default values. Maybe it should point to "Manage the serialization of default properties with @EncodedDefault" instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed the tooltip to include the rest of the information.

image

Also, link points to "Set default values for optional properties" which doesn't mention dropping of default values. Maybe it should point to "Manage the serialization of default properties with @EncodedDefault" instead?

An interesting thought 🤔 — my intention here is to point the user to a place where they can read about the concept of default values.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But with the new structure I think this is resolved! 👍


You can customize these names, called _serial names_,
with the [`@SerialName`](https://kotlinlang.org/api/kotlinx.serialization/kotlinx-serialization-core/kotlinx.serialization/-serial-name/) annotation.
Use it to make a property name shorter or more descriptive in the serialized output:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not think we should advise users making their names shorter


### Validate data in the primary constructor

You can validate constructor parameters before storing it in properties to ensure that a class is serializable and invalid data is rejected during deserialization.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"A deserialization process works like a regular constructor in Kotlin and calls all init blocks" — is an important detail, you didn't have to remove it

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm 🤔 I was/am concerned it makes the section a bit heavier on implementation detail that way. (especially the part about works like a regular constructor)

What do you think about these versions? 🤔

Version 1:

During deserialization, Kotlin calls the class's primary constructor and runs its initializer blocks,
just like when you create an instance.
This allows you to validate constructor parameters and reject invalid data during deserialization.
Here's an example:

Version 2:

To ensure that a class remains serializable and that invalid data is rejected during deserialization, add validation checks in an init block. During deserialization, Kotlin calls the class’s primary constructor and runs all initializer blocks.
Here's an example:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think both of them are somewhat confusing. I think it is relevant here to distinguish between primary constructor classes and those without them. In the first case, the primary constructor (and init blocks) will be called. In the second case, the init blocks will still be called (but a serialization specific constructor is generated that, and none of the existing constructors are called - initialisation inside non-primary constructors will not be invoked).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kotlin calls the class's primary constructor

We don't call the primary constructor 😅 . So this version is more correct:

After deserialization, kotlinx.serialization plugin runs class' initializer blocks,
just like when you create an instance.
This allows you to validate constructor parameters and reject invalid data during deserialization.
Here's an example:

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we could clarify further — what do you think about this? 🤔

During deserialization, Kotlin runs all initializer blocks, and calls the class's primary constructor if it has one.

Shift the focus a little, (initializer blocks are called) and further reinforces that it calls only the primary constructor.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

calls the class's primary constructor if it has one.

We still don't do it 😭 .
kotlinx.serialziation plugin generates a special deserialization constructor, which is called instead of the primary one. This constructor makes sure that all the optional properties are set and all initialization blocks are called.

You can include info about the deserialization constructor if you want, though, but maybe it's low-level enough to omit it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And of course the primary constructor does not have a body anyway (conceptually - technically the initialisation blocks would be), so there is no constructor code to "not" invoke.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We still don't do it 😭 .

Ah sorry the reply didn't show up for me when I was already typing a response to @pdvrieze 😓
(Messages are 1 minute apart 😅 ) Thanks for the extra information! 👍
I agree it feels a bit too low level, so let's go with this one then:

After deserialization, the kotlinx.serialization plugin runs the class's initializer blocks,
just like when you create an instance.
This allows you to validate constructor parameters and reject invalid data during deserialization.
Here's an example:

```
{kotlin-runnable="true"}

### Set default values for optional properties
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summing up all commentaries above, I think this section should be put much earlier — before dropping nulls and mentioning that initializers are not always called for optionals. I think it even worth its own sub-section in the navigation pane on the right. E.g.:

- Optional properties
  - setting default value
  - default values are dropped by default (example with null)
  - initializer is not always called
  - controlling behavior with @EncodeDefault
  - making properties @Required

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the idea 👍 I restructured it

I'm a bit unsure about @Required and @Transient. They mention default values, but I think those might be a better fit in ## Customize class serialization.

But moving everything else about Optional properties solves a lot of issues 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transient indeed fits into ## Customize class serialization.. @Required directly affects properties with optional values (and doesn't make sense without one), so I think it's still better to place it here.

## Optional properties

A property is optional if it isn't required in the serialized input or output.
Properties with _default values_ are optional during serialization.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Properties with _default values_ are optional during serialization.
Properties with _default values_ are optional during deserialization, and can be skipped during serialization, if format is configured to do so. JSON is configured to skip encoding of default values out of the box.

IMO this just repeats the sentence above, better to delete that one and leave the suggested version

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point — I added an extra "for example", before the JSON sentence but let's go with that.

### Serialize nullable properties

Kotlin Serialization natively supports nullable properties.
Like other default values, `null` values aren't encoded in the JSON output:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a slight concern that user may easily overlook "Like other default values" and think that only nulls are not encoded in the output. In the original version, we used example with val language: String = "Kotlin" to demonstrate that it is not encoded. Maybe leave it here as well? Especially given that identical class is used as "Set default values for optional properties" example. Or you can combine both these examples into one, e.g.:

@Serializable
data class Project(val name: String, val language: String = "Kotlin") // Demonstartes non-nullable optional prop

@Serializable
data class User(val name: String, val alias: String? = null) // Demonstrates nullable with null default

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or you can just bring link to "Manage the serialization of default properties with @EncodeDefault" section back, I think link formatting will make this part easier to notice.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both are good ideas. I prefer linking here. It makes the example a bit more concise, so let's go with that 👍

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You probably want to make sure readers are not confused whether or not nulls will be (by default) omitted/optional on reading even if not explicitly given as default.

```
{kotlin-runnable="true" validate="false"}

> If you need to handle `null` values from third-party JSON, you can [coerce them to a default value](serialization-json-configuration.md#coerce-input-values).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe explicitNulls flag is also worth mentioning here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds like a good idea — added it to the tip 👍

```kotlin
// Imports declarations from the serialization and JSON handling libraries
import kotlinx.serialization.*
import kotlinx.serialization.json.*
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it is better to add import kotlinx.serialization.EncodeDefault.Mode.NEVER here and use @EncodeDefault(NEVER)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice 👍
I added it — in addition, I also added some clarification for the opt-in as well, like in the JSON docs. 👍
Please let me know what you think 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants