2

I would like to use Jackson to serialize the output from a stream. Jackson does not have intrinsic support for serializing java.util.stream.Stream, but it is capable of serializing java.util.Iterator.

To illustrate the problem, I would like to serialize this trivial interface:

public interface SerializeMe {
    Iterator<Integer> getMyNumbers();
}

I will be comparing the output from serializing List.iterator() vs. Stream.iterator():

public class SerializeViaList implements SerializeMe {
    @Override
    public Iterator<Integer> getMyNumbers() {
        return Arrays.asList(1, 2, 3).iterator();
    }
}

public class SerializeViaStream implements SerializeMe {
    @Override
    public Iterator<Integer> getMyNumbers() {
        return Arrays.asList(1, 2, 3).stream().iterator();
    }
}

The following method will demonstrate the output from these two classes:

public static void main(String[] args) throws Exception {
    ObjectMapper mapper = new ObjectMapper();

    System.out.println("via list: " + mapper.writeValueAsString(new SerializeViaList()));
    System.out.println("via stream: " + mapper.writeValueAsString(new SerializeViaStream()));
}

The output from this is:

via list: {"myNumbers":[1,2,3]}
via stream: {"myNumbers":{}}

This demonstrates that the stream iterator is not correctly serialized.

Interestingly, it does work if i add @JsonSerialize(as=Iterator.class):

@JsonSerialize(as=Iterator.class)
public Iterator<Integer> getMyNumbers() {
    // ....
}

I don't want to have to write a custom JsonSerializer for every type iterator that a stream can create. What are my alternatives?

Alexey Gavrilov
  • 10,593
  • 2
  • 38
  • 48
jwa
  • 3,239
  • 2
  • 23
  • 54

1 Answers1

5

Iterator is only recognised as an "add-on" interface, i.e. it's only used if no bean serializer was buildable for an object. Unfortunately, the spliterator adaptor does get a dummy bean serializer built since the class has an annotation... this isn't great, and doesn't even seem to be quite what was intended (that isn't an annotation that the inspector uses afaik)

When you specify @JsonSerialize(as=Iterator.class) you're forcing the interpretation as an Iterator, and the IteratorSerializer works fine.

This is a Jackson module I wrote previously to allow serializing a Stream (also LongStream, IntStream or DoubleStream) by serializing its contents sequentially:

public class StreamModule extends SimpleModule {
    public StreamModule() {
        super(StreamModule.class.getSimpleName());
        addSerializer(LongStream.class, new LongStreamSerializer());
        addSerializer(IntStream.class, new IntStreamSerializer());
        addSerializer(DoubleStream.class, new DoubleStreamSerializer());
    }

    @Override
    public void setupModule(SetupContext context) {
        context.addSerializers(new StreamSerializers());
        super.setupModule(context);
    }

    public static class StreamSerializers extends Serializers.Base {
        @Override
        public JsonSerializer<?> findSerializer(SerializationConfig config, JavaType type, BeanDescription beanDesc) {
            Class<?> raw = type.getRawClass();
            if (Stream.class.isAssignableFrom(raw)) {
                JavaType[] params = config.getTypeFactory().findTypeParameters(type, Stream.class);
                JavaType vt = (params == null || params.length != 1) ? TypeFactory.unknownType() : params[0];
                return new StreamSerializer<Object>(config.getTypeFactory().constructParametrizedType(Stream.class, Stream.class, vt), vt);
            }
            return super.findSerializer(config, type, beanDesc);
        }
    }

    static class StreamSerializer<T> extends StdSerializer<Stream<T>> implements ContextualSerializer {
        private final JavaType streamType;
        private final JavaType elemType;

        public StreamSerializer(JavaType streamType, JavaType elemType) {
            super(streamType);
            this.streamType = streamType;
            this.elemType = elemType;
        }

        @Override
        public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property) throws JsonMappingException {
            if (!elemType.hasRawClass(Object.class) && (provider.isEnabled(MapperFeature.USE_STATIC_TYPING) || elemType.isFinal())) {
                JsonSerializer<Object> elemSerializer = provider.findPrimaryPropertySerializer(elemType, property);
                return new TypedStreamSerializer<T>(streamType, elemSerializer);
            }
            return this;
        }

        @Override
        public void serialize(Stream<T> stream, JsonGenerator jgen, SerializerProvider provider) throws IOException,
                JsonGenerationException {
            jgen.writeStartArray();
            try {
                stream.forEachOrdered(elem -> {
                    try {
                        provider.defaultSerializeValue(elem, jgen);
                    } catch (IOException e) {
                        throw new WrappedIOException(e);
                    }
                });
            } catch (WrappedIOException e) {
                throw (IOException) e.getCause();
            }
            jgen.writeEndArray();
        }
    }

    static class TypedStreamSerializer<T> extends StdSerializer<Stream<T>> {
        private final JsonSerializer<T> elemSerializer;

        @SuppressWarnings("unchecked")
        public TypedStreamSerializer(JavaType streamType, JsonSerializer<?> elemSerializer) {
            super(streamType);
            this.elemSerializer = (JsonSerializer<T>) elemSerializer;
        }

        @Override
        public void serialize(Stream<T> stream, JsonGenerator jgen, SerializerProvider provider) throws IOException,
                JsonGenerationException {
            jgen.writeStartArray();
            try {
                stream.forEachOrdered(elem -> {
                    try {
                        elemSerializer.serialize(elem, jgen, provider);
                    } catch (IOException e) {
                        throw new WrappedIOException(e);
                    }
                });
            } catch (WrappedIOException e) {
                throw (IOException) e.getCause();
            }
            jgen.writeEndArray();
        }
    }

    static class IntStreamSerializer extends StdSerializer<IntStream> {
        public IntStreamSerializer() {
            super(IntStream.class);
        }

        @Override
        public void serialize(IntStream stream, JsonGenerator jgen, SerializerProvider provider) throws IOException,
                JsonGenerationException {
            jgen.writeStartArray();
            try {
                stream.forEachOrdered(value -> {
                    try {
                        jgen.writeNumber(value);
                    } catch (IOException e) {
                        throw new WrappedIOException(e);
                    }
                });
            } catch (WrappedIOException e) {
                throw (IOException) e.getCause();
            }
            jgen.writeEndArray();
        }
    }

    static class LongStreamSerializer extends StdSerializer<LongStream> {
        public LongStreamSerializer() {
            super(LongStream.class);
        }

        @Override
        public void serialize(LongStream stream, JsonGenerator jgen, SerializerProvider provider) throws IOException,
                JsonGenerationException {
            jgen.writeStartArray();
            try {
                stream.forEachOrdered(value -> {
                    try {
                        jgen.writeNumber(value);
                    } catch (IOException e) {
                        throw new WrappedIOException(e);
                    }
                });
            } catch (WrappedIOException e) {
                throw (IOException) e.getCause();
            }
            jgen.writeEndArray();
        }
    }

    static class DoubleStreamSerializer extends StdSerializer<DoubleStream> {
        public DoubleStreamSerializer() {
            super(DoubleStream.class);
        }

        @Override
        public void serialize(DoubleStream stream, JsonGenerator jgen, SerializerProvider provider) throws IOException,
                JsonGenerationException {
            jgen.writeStartArray();
            try {
                stream.forEachOrdered(value -> {
                    try {
                        jgen.writeNumber(value);
                    } catch (IOException e) {
                        throw new WrappedIOException(e);
                    }
                });
            } catch (WrappedIOException e) {
                throw (IOException) e.getCause();
            }
            jgen.writeEndArray();
        }
    }

    public static final class WrappedIOException extends RuntimeException {
        private WrappedIOException(IOException e) {
            super(e);
        }
    }
}
araqnid
  • 127,052
  • 24
  • 157
  • 134