my compiler seems to accept the following, however...
# serialization
r
my compiler seems to accept the following, however I'm puzzled, how is this not an error?
Copy code
@Serializable
data class A<T>(
    val a: T
)

class B

val x = A(B()) // B is not serializable
might be relevant to add, it fails as expected, if I make a wrapper like:
Copy code
@Serializable
class Wrapper(
    val foo: A<B>,
)
I'm using kotlin 2.1 and kotlinx-serialization 1.8
m
My guess is that since there's no Serializable interface to apply a bounds to
T
, than the serialization plugin cannot enforce anything when it is building the serializer for
A
so it just has to assume that it will be serializable it tries to serialize it. Then the
val x = A(B())
is not being evaluated by the serialization plugin since it might be in a project that doesn't even apply the plugin, so it only processes classes with the
@Serializable
annotation. Since
T
is unbounded the normal complier has no problem with the type of
x
. And
x
is useable as long as you don't try to serialize it. Now
Wrapper
is not generic, so the plugin can build a serializer that is not generic and while trying to do that, it fails because it cannot pass the correct serializer for
B
into `A`'s serializer. I'm sure some of these details are wrong
r
sounds reasonable though, now I'm just wondering if it would even be possible to fix this 🤔
a
I think that you have to make class B @Serializable too
r
@Ayfri yes, but what I want to fix is that I want to the compiler to tell me that 🙂
a
Ah, idk if it's possible sorry
g
As T is unbounded, it can be any instances. Compiler plugins usually check for signatures but not for runtime (KSP only offers signatures for example). Since you can write
Copy code
val b: Any = if (Random.nextBoolean()) B() else createAnotherObject()
val x = A(b)
checking runtime to ensure all branches are really serializable is very hard, and if even feasible may cost a lot of build time. It sounds more reasonnable to write a linter on your side to detect unbounded generics or other cases you want to prevent. It won't be perfect.
👍🏽 1
e
if you avoid the reified helpers and instead write
Copy code
Json.encodeToString(A.serializer(B.serializer()), x)
then you'd get compile-time errors if there types aren't serializable or don't match
👍🏽 1
r
@ephemient not sure I can use that? My issue in slight more detail is that I'm designing a serializable error type for consistent errors from my API, so something in the lines of
Copy code
data class ApiError<T>(
    val errorId: ErrorId,
    val message: String,
    val supplementaryData: T
)
where supplementary data can be any helpful data structure, e.g.,:
Copy code
data class NucleotideError(
    val characterGiven: Char,
    val validCharacters: List<Char>,
)
that could for DNA be that:
Copy code
{
  "characterGiven": "Q",
  "validCharacters": [
    "A",
    "T",
    "C",
    "G"
  ]
}
so everything is not encoded in a string and can be used with things like i18n frameworks etc.
I'm thinking I might be able to work around that by bounding
T
to an
@Serializable
open class
, but I haven't gotten around to checking that design path yet
e
Copy code
NucleotideError.serializer(ListSerializer(Char.serializer()))
yeah it's more annoying than allowing
serializer<T>()
to pick the right one automatically, but it can't fail at runtime
r
I'm probably misunderstanding here, but won't that require me to know the serializer needed? Thing is that I want a general purpose solution under the hood, that can just do something like
call.respond(error)
that can deal with all my
ApiError
instances
e
at whatever point you perform the serialization, you need to know what the serializer is; the type must be reified there. you're currently getting the serializer "magically", but it's also failing magically
r
hmm, yeah, makes sense, I was also slowly progressing down the reified track 🙂