Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KTOR-7620 Make Url class @Serializable and JVM Serializable #4421

Open
wants to merge 1 commit into
base: 3.1.0-eap
Choose a base branch
from

Conversation

wkornewald
Copy link
Contributor

In our project we had to define our own UrlSerializer. It would be much nicer to have this in the Ktor library itself, so it works out of the box (similar to how Cookie was recently extended).

Also, types like Url and Cookie should be java.io.Serializable. Otherwise Android crashes when using those types as e.g. screen arguments. This happens very quickly when Url is used indirectly as part of a data class where we wanted type safety.

Comment on lines +11 to +19
@Suppress("UNCHECKED_CAST")
public actual fun <T : Any> JvmSerializerReplacement(serializer: JvmSerializer<T>, value: T): Any =
DefaultJvmSerializerReplacement(serializer, value)

@PublishedApi // IMPORTANT: changing the class name would result in serialization incompatibility
internal class DefaultJvmSerializerReplacement<T : Any>(
private var serializer: JvmSerializer<T>?,
private var value: T?
) : Externalizable {
Copy link
Contributor Author

@wkornewald wkornewald Oct 22, 2024

Choose a reason for hiding this comment

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

Should such an API better be integrated as part of the Kotlin stdlib itself or at least in a separate library for reuse in other projects? It could also be useful in kotlinx-datetime: Kotlin/kotlinx-datetime#373
And I'll also add the same code to my open-source lib, too (https://github.com/ensody/ReactiveState-Kotlin).
That's unnecessary duplication in multiple places.

Copy link
Member

Choose a reason for hiding this comment

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

Regarding JvmSreializable interface, you can vote on this issue:

  • KT-48243 Publish kotlin.io.Serializable

I haven't found any issue proposing multiplatform Externalizable implementation, so feel free to create one!

}
}

internal object UrlJvmSerializer : JvmSerializer<Url> {
Copy link
Contributor Author

@wkornewald wkornewald Oct 27, 2024

Choose a reason for hiding this comment

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

Just an idea: To make this slightly easier to implement one could also add a reusable serializer which utilizes kotlinx-serialization (e.g. using JSON or BSON or CBOR internally).

Copy link
Member

Choose a reason for hiding this comment

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

I like this idea! Would you like to implement it in this PR or in a separate one?

@e5l e5l self-requested a review October 29, 2024 07:35
@e5l e5l changed the base branch from main to 3.1.0-eap October 29, 2024 07:36
@osipxd osipxd force-pushed the 3.1.0-eap branch 2 times, most recently from a65ff10 to 9ae3d49 Compare October 31, 2024 10:15
In our project we had to define our own UrlSerializer. It would be much nicer to have this in the Ktor library itself, so it works out of the box (similar to how Cookie was recently extended).

Also, types like Url and Cookie should be java.io.Serializable. Otherwise Android crashes when using those types as e.g. screen arguments. This happens very quickly when Url is used indirectly as part of a data class where we wanted type safety.
Copy link
Member

@e5l e5l left a comment

Choose a reason for hiding this comment

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

Could you tell me why can't we mark it with @serializable from kotlinx.serialization?

@wkornewald
Copy link
Contributor Author

Could you tell me why can't we mark it with @serializable from kotlinx.serialization?

This PR is doing that, but that's not enough.

On Android many APIs work with anything that's Parcelable/Serializable. Imagine you have some random data class which contains a Url. You pass that object into an Android API and that tries to parcel/serialize it. There is no kotlinx.serialization involved in this case.

It's very error-prone and inconvenient to force everyone to check all the attributes of every data class and manually use kotlinx.serialization in many different places to ensure that the data is compatible with the Android API. The better solution is to make all widely used types compatible with java.io.Serializable or at least Parcelable. It's the same issue with kotlinx-datetime where people also run into crashes because Instant is incompatible with Android and nobody wants to manually write serializers and remember to use JSON serialization in thousands of places. Everything should just work without crashes and without thinking.

For this to be possible, the types that are very commonly used in the UI like Url, Instant, etc. need to be compatible with java.io.Serializable. But we can also chat or have a quick Huddle on the JetBrains Slack. I'm available there under "Waldemar Kornewald".

@e5l
Copy link
Member

e5l commented Nov 7, 2024

That sounds good; let's mark parent interfaces with the @InternalApi annotation.

@osipxd, could you also check?

@e5l e5l requested review from e5l and osipxd and removed request for bjhham November 7, 2024 09:06
@@ -254,3 +262,22 @@ internal val Url.encodedUserAndPassword: String
get() = buildString {
appendUserAndPassword(encodedUser, encodedPassword)
}

public class UrlSerializer : KSerializer<Url> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Url", PrimitiveKind.STRING)
Copy link
Member

Choose a reason for hiding this comment

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

Let's use a qualified name

Suggested change
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Url", PrimitiveKind.STRING)
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("io.ktor.http.Url", PrimitiveKind.STRING)

}
}

internal object UrlJvmSerializer : JvmSerializer<Url> {
Copy link
Member

Choose a reason for hiding this comment

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

I like this idea! Would you like to implement it in this PR or in a separate one?

Comment on lines +11 to +19
@Suppress("UNCHECKED_CAST")
public actual fun <T : Any> JvmSerializerReplacement(serializer: JvmSerializer<T>, value: T): Any =
DefaultJvmSerializerReplacement(serializer, value)

@PublishedApi // IMPORTANT: changing the class name would result in serialization incompatibility
internal class DefaultJvmSerializerReplacement<T : Any>(
private var serializer: JvmSerializer<T>?,
private var value: T?
) : Externalizable {
Copy link
Member

Choose a reason for hiding this comment

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

Regarding JvmSreializable interface, you can vote on this issue:

  • KT-48243 Publish kotlin.io.Serializable

I haven't found any issue proposing multiplatform Externalizable implementation, so feel free to create one!

val encoded = ByteArrayOutputStream().also {
ObjectOutputStream(it).writeObject(obj)
}.toByteArray()
return ObjectInputStream(encoded.inputStream()).readObject() as T
Copy link
Member

Choose a reason for hiding this comment

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

Should the assertEquals call be right here?

Suggested change
return ObjectInputStream(encoded.inputStream()).readObject() as T
val decoded = ObjectInputStream(encoded.inputStream()).readObject() as T
assertEquals(obj, decoded)
return decoded

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would have to be optional (default true) because not all objects might be equal after deserialization, though in most cases they would be. But yes, that's actually how we do in our implementation. I was just afraid of the code looking 100% the same...

@@ -254,3 +262,22 @@ internal val Url.encodedUserAndPassword: String
get() = buildString {
appendUserAndPassword(encodedUser, encodedPassword)
}

public class UrlSerializer : KSerializer<Url> {
Copy link
Member

Choose a reason for hiding this comment

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

It could be an object. And probably make it inner object Url.Serializer

Suggested change
public class UrlSerializer : KSerializer<Url> {
public object UrlSerializer : KSerializer<Url> {

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.

3 participants