question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

(datatypes) Add Serialization Support for Streams

See original GitHub issue

(moved from earlier issue filed by @jmax01)

Here is a first pass at serializing Streams. It works for 2.6.x and above. A 2.8.1 version is also shown.


import java.io.IOException;
import java.lang.reflect.Type;
import java.util.Iterator;
import java.util.stream.Stream;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.AnnotationIntrospector;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BasicSerializerFactory;
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
import com.fasterxml.jackson.databind.ser.std.AsArraySerializerBase;
import com.fasterxml.jackson.databind.type.CollectionLikeType;
import com.fasterxml.jackson.databind.type.TypeBindings;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.databind.type.TypeModifier;

/**
 * The Class StreamModule.
 *
 * @author jmaxwell
 */
public class StreamModule extends SimpleModule {

    /** The Constant serialVersionUID. */
    private static final long serialVersionUID = -1324033833221219001L;

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

    /**
     * The Class StreamTypeModifier.
     */
    public static final class StreamTypeModifier extends TypeModifier {

        /**
         * Tested for both 2.6.x and 2.8.1
         */
        @Override
        public JavaType modifyType(final JavaType type, final Type jdkType, final TypeBindings context,
                final TypeFactory typeFactory) {

            if (type.isReferenceType() || type.isContainerType()) {
                return type;
            }

            final Class<?> raw = type.getRawClass();

            if (Stream.class.isAssignableFrom(raw)) {

                final JavaType[] params = typeFactory.findTypeParameters(type, Stream.class);

                if (params == null || params.length == 0) {

                    return typeFactory.constructReferenceType(raw, TypeFactory.unknownType());
                }

                return typeFactory.constructCollectionLikeType(raw, params[0]);
            }
            return type;
        }

        //
        // the 2.8.1 and above way
        // @Override
        // public JavaType modifyType(JavaType type, Type jdkType, TypeBindings context, TypeFactory typeFactory) {
        //
        // if (type.isReferenceType() || type.isContainerType()) {
        // return type;
        // }
        //
        // Class<?> raw = type.getRawClass();
        //
        // if (Stream.class.isAssignableFrom(raw)) {
        //
        // JavaType[] params = typeFactory.findTypeParameters(type, Stream.class);
        //
        // if (params == null || params.length == 0) {
        //
        // return ReferenceType.upgradeFrom(type, type.containedTypeOrUnknown(0));
        // }
        //
        // return typeFactory.constructCollectionLikeType(raw, params[0]);
        //
        // }
        // return type;
        // }
        //

    }

    /**
     * The Class StreamSerializers.
     */
    public static final class StreamSerializers extends com.fasterxml.jackson.databind.ser.Serializers.Base {

        @Override
        public JsonSerializer<?> findCollectionLikeSerializer(final SerializationConfig config,
                final CollectionLikeType type, final BeanDescription beanDesc,
                final TypeSerializer elementTypeSerializer, final JsonSerializer<Object> elementValueSerializer) {

            final Class<?> raw = type.getRawClass();

            if (Stream.class.isAssignableFrom(raw)) {

                final TypeFactory typeFactory = config.getTypeFactory();

                final JavaType[] params = typeFactory.findTypeParameters(type, Stream.class);

                final JavaType vt = (params == null || params.length != 1) ? TypeFactory.unknownType() : params[0];

                return new StreamSerializer(type.getContentType(), usesStaticTyping(config, beanDesc, null),
                        BeanSerializerFactory.instance.createTypeSerializer(config, vt));
            }

            return null;
        }

        /**
         * Uses static typing. Copied from {@link BasicSerializerFactory}
         *
         * @param config the config
         * @param beanDesc the bean desc
         * @param typeSer the type ser
         * @return true, if successful
         */
        private static final boolean usesStaticTyping(final SerializationConfig config, final BeanDescription beanDesc,
                final TypeSerializer typeSer) {
            /*
             * 16-Aug-2010, tatu: If there is a (value) type serializer, we can not force
             * static typing; that would make it impossible to handle expected subtypes
             */
            if (typeSer != null) {
                return false;
            }
            final AnnotationIntrospector intr = config.getAnnotationIntrospector();
            final JsonSerialize.Typing t = intr.findSerializationTyping(beanDesc.getClassInfo());
            if (t != null && t != JsonSerialize.Typing.DEFAULT_TYPING) {
                return (t == JsonSerialize.Typing.STATIC);
            }
            return config.isEnabled(MapperFeature.USE_STATIC_TYPING);
        }

        /**
         * The Class StreamSerializer.
         */
        public static final class StreamSerializer extends AsArraySerializerBase<Stream<?>> {

            /** The Constant serialVersionUID. */
            private static final long serialVersionUID = -455534622397905995L;

            /**
             * Instantiates a new stream serializer.
             *
             * @param elemType the elem type
             * @param staticTyping the static typing
             * @param vts the vts
             */
            public StreamSerializer(final JavaType elemType, final boolean staticTyping, final TypeSerializer vts) {
                super(Stream.class, elemType, staticTyping, vts, null);
            }

            /**
             * Instantiates a new stream serializer.
             *
             * @param src the src
             * @param property the property
             * @param vts the vts
             * @param valueSerializer the value serializer
             */
            public StreamSerializer(final StreamSerializer src, final BeanProperty property, final TypeSerializer vts,
                    final JsonSerializer<?> valueSerializer) {
                super(src, property, vts, valueSerializer, false);
            }

            @Override
            public void serialize(final Stream<?> value, final JsonGenerator gen, final SerializerProvider provider)
                    throws IOException {
                this.serializeContents(value, gen, provider);
            }

            /**
             * withResolved.
             *
             * @param property the property
             * @param vts the vts
             * @param elementSerializer the element serializer
             * @param unwrapSingle ignored always false since streams are one time use I don't believe we can get a
             *            single element
             * @return the as array serializer base
             */
            @Override
            public StreamSerializer withResolved(final BeanProperty property, final TypeSerializer vts,
                    final JsonSerializer<?> elementSerializer, final Boolean unwrapSingle) {
                return new StreamSerializer(this, property, vts, elementSerializer);
            }

            @Override
            protected void serializeContents(final Stream<?> value, final JsonGenerator gen,
                    final SerializerProvider provider) throws IOException {

                provider.findValueSerializer(Iterator.class, null)
                    .serialize(value.iterator(), gen, provider);

            }

            @Override
            public boolean hasSingleElement(final Stream<?> value) {
                // no really good way to determine (without consuming stream), so:
                return false;
            }

            @Override
            protected StreamSerializer _withValueTypeSerializer(final TypeSerializer vts) {

                return new StreamSerializer(this, this._property, vts, this._elementSerializer);
            }
        }
    }
}

Tests:


import static org.junit.Assert.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Stream;

import org.junit.Test;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.module.afterburner.AfterburnerModule;
import com.fasterxml.jackson.module.mrbean.MrBeanModule;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import com.theice.cds.common.serialization.json.jackson2.StreamModule;

@SuppressWarnings("javadoc")
public class StreamModuleTest {

    public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(new GuavaModule())
        .registerModule(new Jdk8Module())
        .registerModule(new JavaTimeModule())
        .registerModule(new ParameterNamesModule())
        .registerModule(new AfterburnerModule())
        .registerModule(new StreamModule())
        .registerModule(new MrBeanModule());

    static <T> void assertRoundTrip(final Collection<T> original, final ObjectMapper objectMapper) throws IOException {

        final Stream<T> asStream = original.stream();

        final String asJsonString = objectMapper.writeValueAsString(asStream);

        System.out.println("original: " + original + " -> " + asJsonString);

        final Collection<T> fromJsonString = OBJECT_MAPPER.readValue(asJsonString,
                new TypeReference<Collection<T>>() {});

        assertEquals(original, fromJsonString);
    }

    @SuppressWarnings("deprecation")
    @Test
    public void testEmptyStream() throws IOException {

        assertRoundTrip(new ArrayList<>(), OBJECT_MAPPER.copy()
            .enable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT));

        // shouldn't this fail?
        assertRoundTrip(new ArrayList<>(), OBJECT_MAPPER.copy()
            .disable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS)
            .disable(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT));
    }

    @Test
    public void testSingleElementStream() throws IOException {

        final List<String> collection = new ArrayList<>();
        collection.add("element1");

        assertRoundTrip(collection, OBJECT_MAPPER.copy()
            .enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)
            .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY));

        assertRoundTrip(collection, OBJECT_MAPPER.copy()
            .disable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)
            .disable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY));

        // should fail but can't for stream
        assertRoundTrip(collection, OBJECT_MAPPER.copy()
            .enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)
            .disable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY));
    }

    @Test
    public void testMultipleElementStream() throws IOException {

        final List<String> collection = new ArrayList<>();
        collection.add("element1");
        collection.add("element2");

        assertRoundTrip(collection, OBJECT_MAPPER);

    }
}

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Reactions:10
  • Comments:12 (6 by maintainers)

github_iconTop GitHub Comments

1reaction
whiskeysierracommented, May 17, 2018

Perhaps streams are not really meant to work with things bound to real resources, and are more “ephemeral” generators.

Not necessarily. They implement AutoClosable and are meant to be used for actual resources.

1reaction
cowtowncodercommented, Sep 8, 2017

Ok, and just to be sure, I wouldn’t be opposed to something like, say, MappingStream as counterpart to MappingIterator. My point was just that I am trying to remove direct use of ObjectMapper as much as possible due to combinatorial explosion of things (and inability to reconfigure).

I mean, MappingIterator is pretty simple so anyone with itch and interest would be welcome to play with this idea.

And since we now have ng-3.0 branch, this could be started already.

Then again I have used so little Java 8 that there’s plenty of learning to do. Perhaps streams are not really meant to work with things bound to real resources, and are more “ephemeral” generators.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Kafka Streams Data Types and Serialization
Every Kafka Streams application must provide Serdes (Serializer/Deserializer) for the data types of record keys and record values (e.g. java.lang.
Read more >
Data Types and Serialization - Apache Kafka
Every Kafka Streams application must provide SerDes (Serializer/Deserializer) for the data types of record keys and record values (e.g. java.lang.
Read more >
Data Types and Serialization: Flink Advanced Tutorials
A serializer is required to implement serialization in Flink. Each data type corresponds to a specific TypeInformation, which provides the data ...
Read more >
Serialization Formats - ksqlDB Documentation
The AVRO format supports Avro binary serialization of all SQL data types, including records and top-level primitives, arrays, and maps. Note.
Read more >
Serialization and custom serializers - Orleans - Microsoft Learn
Serializers are registered for each supported data type at silo start-up and whenever an assembly is loaded. Registration is necessary for ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found