Jackson and that dreaded IOException
Asked Answered
R

4

17

Jackson's ObjectMapper#readValue member throws three checked exceptions:

IOException 
JsonParseException 
JsonMappingException

JsonParseException and JsonMappingException extend IOException. I want to wrap the two aforementioned child classes and throw my own custom exceptions, yet, the base class, IOException, being checked, requires me to either catch or throw it also.

It doesn't make sense for me to throw the IOException up to the calling layer, but, adversely, it's a smell if I hide it. My original thought was to not catch it and leave it to the caller/run-time exception mechanism to deal with it... yet, I don't want to have to force the caller to catch or specify.

What does one do in such a situation?

Rugen answered 19/9, 2011 at 12:51 Comment(0)
H
15

Short answer: if you deal with IO, you deal with IOExceptions. If you don't deal with IO, then IOExceptions should be turned into unchecked exceptions, because they're symptoms of buggy code.


Longer answer:

readValue always takes a JsonParser, which may be wrapped around IO (e.g. a file or a URL). If you're dealing with IO, there's no way around dealing with IOExceptions, and you should either handle them or rethrow/pass them in some way. Anything can happen during IO, and you should be prepared to deal with the exceptions.

However, if you are sure that your JsonParser instances don't use IO (e.g. you used JsonFactory#createJsonParser(java.lang.String) to create a JSON parser on a string), you may assume that any IOExceptions you receive are bugs, either in your code or in Jackson. Usually, throwing an unchecked exception is then the proper way to deal with it:

ObjectMapper om = new ObjectMapper(/* whatever */);
JsonParser jp = JsonFactory.createJsonParser("{ \"foo\": \"bar\" }");
try {
    return om.readValue(jp);
} catch (IOException e) {
    throw new AssertionError("An IOException occurred when this was assumed to be impossible.");
}

Nota bene: my Java is rusty and I've never used Jackson, so consider the above block to be pseudocode.

In any case, you never need to declare AssertionError in throws, because they're unchecked exceptions. Everything that's a subclass of java.lang.RuntimeException or java.lang.Error doesn't need to be caught or rethrown explicitly. These exceptions are used for problems that are not expected to occur unless you're dealing with buggy code or when your VM's host is on fire.

Highstepper answered 19/9, 2011 at 13:26 Comment(8)
+1: I like idea that you either have to deal with the exception or "declare it impossible" by wrapping in a RuntimeException.Ingleside
@Trinctorius - "As to when each type is thrown, basic rule is that Jackson tries to pass underlying IOExceptions from read source (InputStream, Reader), and in limited number of cases where problem is related to low-level character decoding (invalid UTF-8 sequence)." I'm passing a String to ObjectMapper#readValue. From my perspective, no IO is really happening.Rugen
@wulfgar.pro I covered that exact situation as well, didn't I? ;)Highstepper
@Trinctorius - I guess, for me, the confusion came from not knowing the exact reason as to why the ObjectMapper#readValue member throws IOException. Knowing that it's a possibility if passing in a stream an IOException might occur, clears things up. It doesn't seem correct that I should be forced to catch or declare the IOException if I'm using the override for readValue that accepts a String.Rugen
I understand the confusion and I agree readValue shouldn't throw an IOException for in case of String. I think Jackson is not really well-typed and should treat IO separately from non-IO (but that's probably because I have to do that as well in Haskell ;)).Highstepper
@Tinctorius -- while I can see a case for suppressing IOExceptions for case where String is passed, this is due to the fact that after initializations, Strings are read via StringReader, passed as Reader, and from thereon core code has no knowledge that IOExceptions aren't expected. Caller could then try catching "impossible" exceptions, and maybe this is worth an RFE? It would make calls bit less consistent (and for me I sort of prefer consistency in API), but if users want it, they should request it.Pax
Now that I took a better look, the Jackson API is not consistent. 1) Invalid UTF-8 sequences are parsing errors, never IO exceptions. Stuff was successfully read from the data source, but the stuff didn't make sense. 2) Declaring more throws than needed is inconsistent with the actual contract of a function. String instances are always valid, so IOException should never be thrown from <T> T ObjectMapper#readValue(java.lang.String, java.lang.Class<T> valueType).Highstepper
@Tinctorius - thanks for validating my thoughts. I think the best course of action will be to catch and throw a RuntimeException as you have pointed out in your answer.Rugen
P
2

While this is not clearly documented within Jackson, rationale for IOExceptions is simple: IOExceptions from input sources (and output targets) are thrown as is -- since Jackson itself can not do anything for these, they are thrown as is. The only additional source for IOExceptions are things that are conceptually part of low-level (data-format independent) I/O handling, specifically, decoding of characters encodings like UTF-8.

From this, it seems relatively intuitive that JsonParsingException is for problems related to trying to parse invalid content; and JsonMappingException for problems at data-binding level. a

Pax answered 20/9, 2011 at 16:42 Comment(4)
sure, but what about the native IOExcetion thrown in addition?Rugen
Only additional IOExceptions (not thrown by Readers/Writers, Input/OutputStreams) should be due to character decoding, which is logically part of Reader/Writers. Or am I misunderstanding your question?Pax
So, if I'm passing in a String, I must catch/throw the additional IOException due to the underlying code decoding characters from my String into UTF-8? When would such an exception occur? If I was to pass in a UTF-16 character with no UTF-8 derivative (single byte)?Rugen
No, String characters are never decoded into UTF-8 (they are encoded as UTF-8 for writing), since internal representation (basically UCS-2) is used for parsing. So declaration of throwing IOException is a by-product of java.io.Reader declaring IOException being thrown (even if StringReader does not). ObjectMapper could conceivably add bogus catch block, and so we could get rid of this superfluous exception -- if this sounds like a good idea, maybe file a Jira RFE to request it? (so it can be discussed on list etc)Pax
I
2

May patience got dried while wrapping dozen on methods in try/catch/rethrow boilerplate code. And I found Lombok can handle it for me. Hope it helps https://projectlombok.org/features/SneakyThrows

Inflexible answered 14/6, 2021 at 13:43 Comment(5)
That's throwing out the baby with the bathwater, as you taint your entire method with that annotation, not just the statement you're trying to silence.Buran
@SneakyThrows doesn't silence exceptions. It re-throws them as runtimes (unchecked), emending the original one as its source. These extreme cases, were you want part of IOExceptions in method to be checked ones and part - unchecked - is a really strange design, that might be a sign of violation of single responsibility principle.Inflexible
It silences the compiler obviously.Buran
... and the very point of this case is to silence the compiler! :) P.S. Most projects I've used to worked on explicitly banned checked exceptions due to code pollution.Inflexible
It should be the accepted answer. Checked exceptions have been misused for years.Adeleadelheid
K
1

You should handle the IOException the same way you handle the json exceptions and wrap it. As the documentation of Jackson is lacking so much, you don't really know why any of them are thrown anyway (except for "an unknown error").

Kimble answered 19/9, 2011 at 13:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.