3

There seems to be a problem with serializing BufferedImages in JSON using GSON. I am using Derby to store images. When I query the the database I build a JavaBean that has some text fields and one BufferedImage field. I then use GSON to convert the JavaBean into JSON, and this is where the exception occurs.

The Exception message is below:
java.lang.IllegalArgumentException: class sun.awt.image.ByteInterleavedRaster declares multiple JSON fields named maxX

I did find similar problems here GSON java.lang.IllegalArgumentException: class 'xx' declares multiple JSON fields named 'XX' AND StackOverflowError and here class A declares multiple JSON fields

But the problem is with the awt library included with Java. I could follow the answers provided in those other stackoverflow answers if I could access the AWT source code, but how do I do that?

Community
  • 1
  • 1
kiwicomb123
  • 1,503
  • 21
  • 26

1 Answers1

4

You have to know that not every class is designed to be (de)serialized, especially if (de)serialization is based on the target class binary structure. Your approach has at least the following weak points:

  • the sun.awt.image.ByteInterleavedRaster class fields are not necessarily the same on another JVM/JRE, thus you can get be vendor-locked;
  • persisting binary data in JSON is probably not the best choice (probably huge and terrible memory consumption during (de)serialization, storage consumption, performance) -- maybe a generic blob storage is better for binary data?
  • reading an images with Java AWT and writing it back does not guarantee the same binary output: for example, my test image, 1.2K, was deserialized as an image of another size, 0.9K;
  • you must choose the target persisting image format or detect the most efficient one (how?).

Consider the following simple class:

final class ImageHolder {

    final RenderedImage image;

    ImageHolder(final RenderedImage image) {
        this.image = image;
    }

}

Now you have to create a type adapter to tell Gson how a particular type instance can be stored and restored:

final class RenderedImageTypeAdapter
        extends TypeAdapter<RenderedImage> {

    private static final TypeAdapter<RenderedImage> renderedImageTypeAdapter = new RenderedImageTypeAdapter().nullSafe();

    private RenderedImageTypeAdapter() {
    }

    static TypeAdapter<RenderedImage> getRenderedImageTypeAdapter() {
        return renderedImageTypeAdapter;
    }

    @Override
    @SuppressWarnings("resource")
    public void write(final JsonWriter out, final RenderedImage image)
            throws IOException {
        // Intermediate buffer
        final ByteArrayOutputStream output = new ByteArrayOutputStream();
        // By the way, how to pick up the target image format? BMP takes more space, PNG takes more time, JPEG is lossy...
        ImageIO.write(image, "PNG", output);
        // Not sure about this, but converting to base64 is more JSON-friendly
        final Base64.Encoder encoder = Base64.getEncoder();
        // toByteArray() returns a copy, not the original array (x2 more memory)
        // + creating a string requires more memory to create the String internal buffer (x3 more memory)
        final String imageBase64 = encoder.encodeToString(output.toByteArray());
        out.value(imageBase64);
    }

    @Override
    public RenderedImage read(final JsonReader in)
            throws IOException {
        // The same in reverse order
        final String imageBase64 = in.nextString();
        final Base64.Decoder decoder = Base64.getDecoder();
        final byte[] input = decoder.decode(imageBase64);
        return ImageIO.read(new ByteArrayInputStream(input));
    }

}

Note that Gson is currently NOT very well designed to support byte transformation, however it might be somewhat better in the future if fixed.

Example use:

private static final Gson gson = new GsonBuilder()
        .registerTypeHierarchyAdapter(RenderedImage.class, getRenderedImageTypeAdapter())
        .create();

public static void main(final String... args)
        throws IOException {
    try ( final InputStream inputStream = getPackageResourceInputStream(Q43301580.class, "sample.png") ) {
        final RenderedImage image = ImageIO.read(inputStream);
        final ImageHolder before = new ImageHolder(image);
        final String json = gson.toJson(before);
        System.out.println(json);
        final ImageHolder after = gson.fromJson(json, ImageHolder.class);
        ...
    }
}

Example output (with real tiny (32x32) PNG file inside):

{"image":"iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADgklEQVR42t2XXUiTYRTHpxj4kSKShhgYGSihZGIXXYhU5J2BhBIhCH5cCF6oiWhG0k1BpHghgRgoJHiloBKEqFQ3frDNuemaOqdu0+n8mFM3Nzf37z1n+JZUEPlOoQdetvd5L87vOed/Ph4ZznnJzsqQz+uFz+M5HwBrezuUFy9CERoKY3U1jtzuwAFY29pgGxgQ350aDVSXLmFfLud9eVAQTHV1gQNYKi+HMiwM9uFhft/o6MBcTg6fWp+XB93duzhyOOA7POSwyAIR64UnTxhi9+tXfhQhIdBlZ2P2wQM2Tmv11StY3rwJjAYIQl9QAGVUFPZGRzF7/z7kwcGw9ffzt80PHzAZE4ODuTnpAQ50OjgmJ3HkcmE+N5chdr98wfzDh5DLZPyo4uOx+/mz9Bqg+B8b0d6+zSecFeJPInSo1XAbjXAKvxR/yUW4Pz7uV/vEBJ9OffUqNNev49BiYeGp4uLg0usDUwdIUNNpaTDV1op7rqUljvNKYyMLb7G4GIdWa2AAbH19LDIy8vNaefmSBRiQUkynMtXUYLGkBO7lZWx2dTEEnVjURFnZL1CSASyWlmL6xg1okpIwdeUK3CYTNjo7WYCGoiLOeU1yMtxmc2AA1NeuscA829uYTk1lEIJYf/eOIcgzP6tdEgAyRicjtatiY8V9EhdDpKTw/7XmZoYgGEkBzEITIQDzs2dsYPX1a/EbuZq8YG5o8GeG8E2dmIgjp/P0AJxGgku1GRnYVyh479jVdFrRE+vrXGqPl3dvTxoPeO12aDMz2aBDqRT315qa/trV/wTgsdmw1d3NJVSMs+BmOqlYhARXL1dUSA/gWljg9FKGh/u72tgYQ1BqEcjvqtqpAHY+fcLOx4/+durzcTOxvH3LXY1qOUFQ/CnVyAszN2+eGK1OBWCur4cyIgIrL174Xb+1hdl79xiERioqOFRSKf3sQ0MclvXWVmk8sN3b6+9UBsMvQwWtb3fuwD4ywpkwlZDAojNWVUk3lhsrK7Hw+PHJ+AudzKnVwrOzwwYP5ud50JhJT5cs9iLAxvv3UFy4wLVdn58P1eXLP4YKIfWor09GR0MZGYm1lhbpLyYUZ/Pz55i5dQu6rCwYnz4FhYXmNjJKKbYmiHG7p+fsb0aGwkIsC2PWuVzNaJ5j1Q8Oni0AVTkKCbmffs/8cuoVlK9/9IjHrP/qdvyn9R0SEM4flWsmCwAAAABJRU5ErkJggg\u003d\u003d"}

I think there are too many flaws, and I would strongly recommend you to redesign your binaries storage if possible and store binary content as-is.

Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
  • You mean just keep it as a binary right from the client side? i.e don't do any conversation at all on the server side? – kiwicomb123 Apr 14 '17 at 05:21
  • @kiwicomb123 It depends on what you really need in your case. Using a buffered image will result into an image conversion (is it acceptable?). Also, passing it via JSON _using_ Gson may be not the best choice, because Gson requires the whole string to be read/written from/to the JSON stream, not by chunks (maybe other JSON libraries can do it better?). By simply keeping it binary and storing it as is (you might apply some validation before upload, of course), you could just expose the binary via a direct URL (sure, some authorization might be applied too). – Lyubomyr Shaydariv Apr 14 '17 at 05:31
  • @kiwicomb123 Alternatively you might also experiment with https://fasterxml.github.io/jackson-core/javadoc/2.6/com/fasterxml/jackson/core/JsonGenerator.html and http://fasterxml.github.io/jackson-core/javadoc/2.6/com/fasterxml/jackson/core/JsonParser.html -- they seem to support raw data so you could attempt to combine various input/output streams in order to produce/validate your data and write it directly to JSON. – Lyubomyr Shaydariv Apr 14 '17 at 05:50