kotlinx.serialization icon indicating copy to clipboard operation
kotlinx.serialization copied to clipboard

Add a less verbose error message to SerializationException

Open pnq32 opened this issue 3 years ago • 11 comments

Use case: In my current use case, I am passing user-provided JSON input to the Json.decodeFromString function. In general, I expect this input to be erroneous. It could, for instance, contain a key that is not part of the actual data model. I want to make the user aware of such issues by printing a suitable error message.

Limitation: If I am not mistaken, the only possibility for me to detect such error conditions is by catching SerializationException. Unfortunately, the error message encoded into this Exception seems to be directed towards the developer and is not just objectively describing the issue. It looks as follows, for instance:

kotlinx.serialization.json.internal.JsonDecodingException: Unexpected JSON token at offset 4: Encountered
an unknown key 'name'. Use 'ignoreUnknownKeys = true' in 'Json {}' builder to ignore unknown keys.
JSON input: {
  "name": "xyz"
}

Obviously, mentioning ignoreUnknownKeys to the user would be highly confusing.

Proposal: This specific problem could be solved by adding an optional SerializationException::shortMesssage property and populating it with error messages that focus solely on the client-controlled portions of the serialization/deserialization process (whenever this is applicable). Do you think this would be feasible? Or am I missing something here?

Alternatively, it might make sense to create public SerializationException subclasses that notify the caller about one specific error condition (such as the presence of an unknown field). This would probably be the cleanest and most flexible solution, but it would also require quite a bit of additional effort.

pnq32 avatar Mar 02 '22 21:03 pnq32

I think in your particular case it's easier to just do substring on the message. But in general, idea of messages/data addressed to the user may be interesting.

sandwwraith avatar Mar 29 '22 12:03 sandwwraith

Thank you for your response.

I looked into the code that throws these Exceptions. Unfortunately, however, I was unable to recognize a "pattern" that I could use to parse the messages in a reasonable manner. Looking at code portions throwing JsonDecodingException, I can see that:

  1. JsonException.kt produces eight different kinds of error messages.
  2. TreeJsonDecoder.kt produces six additional messages.
  3. JsonElementSerializers.kt produces about three additional messages.
  4. TreeJsonEncoder.kt and Polymorphic.kt contribute to the message pool as well.

In addition, other kinds of Exceptions seem to be thrown as well, such as InvalidKeyKindException from here.

Do you have an idea of how a such a parsing strategy might look like?

pnq32 avatar Apr 10 '22 08:04 pnq32

For example in the MissingFieldException case the fields are hidden so as far as I can see it's pretty hard to even have a wrapper around that theoretically could do this type of post parsing production of more human readable messages.

dahankzter avatar Apr 11 '23 12:04 dahankzter

I kind of like what it yields now and I would just like the possibility to use the class simple name instead of the fully qualified package and class name.

dahankzter avatar Apr 11 '23 13:04 dahankzter

@dahankzter MissingFieldException is public from 1.4.0

sandwwraith avatar Apr 11 '23 13:04 sandwwraith

I have this:

@PublishedApi
internal class MissingFieldException
// This constructor is used by coroutines exception recovery
internal constructor(message: String?, cause: Throwable?) : SerializationException(message, cause) {
    // This constructor is used by the generated serializers
    constructor(fieldName: String) : this("Field '$fieldName' is required, but it was missing", null)
    internal constructor(fieldNames: List<String>, serialName: String) : this(if (fieldNames.size == 1) "Field '${fieldNames[0]}' is required for type with serial name '$serialName', but it was missing" else "Fields $fieldNames are required for type with serial name '$serialName', but they were missing", null)
}

But then again I am on 1.3.3... Any gotchas to upgrade?

dahankzter avatar Apr 11 '23 13:04 dahankzter

Do you have an idea of how a such a parsing strategy might look like?

I would strongly avoid relying on the message parsing -- it's intended to be extensive, verbose and may change from version to version within patch releases. The parsing itself is rather a mean to address our lack of proper API to distinguish failure's reason.

It would be really helpful if you could list what exactly you are trying to achieve (e.g. what kinds of failures you are interested in, what kinds you are ignoring, what kind of additional information you would like to parse out etc.), so we can shape the API to be usable, future-proof and extensive enough for your use-cases

qwwdfsad avatar Apr 11 '23 13:04 qwwdfsad

MissingFieldException in 1.4.1 (after some dependency wrangling in ktaor) works sort of for my case but it is still half baked for validations I would say. IT only exposes the fields that are missing but not the target class. I can parse the message but that quickly becomes cumbersome and hard to maintain.

I am actually doing this in a KTor project and if someone has already solved it for that case (nice non-nullable fields with as little as possible "external" validation) a pointer would be nice.

dahankzter avatar Apr 11 '23 13:04 dahankzter

@qwwdfsad @dahankzter I think we can add serialName: String? too, because it is available in recent plugins.

sandwwraith avatar Apr 11 '23 13:04 sandwwraith

That would be awesome! I can't speak for other cases but great for us.

dahankzter avatar Apr 11 '23 15:04 dahankzter