JsonUtil.java

/**
 * Copyright (c) 2017, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.commons.json;

import com.fasterxml.jackson.core.JsonFactory;
import com.fasterxml.jackson.core.JsonFactoryBuilder;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.core.json.JsonWriteFeature;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.google.common.base.Strings;
import com.google.common.base.Suppliers;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.extensions.Extendable;
import com.powsybl.commons.extensions.Extension;
import com.powsybl.commons.extensions.ExtensionJsonSerializer;
import com.powsybl.commons.extensions.ExtensionProviders;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * @author Mathieu Bague {@literal <mathieu.bague at rte-france.com>}
 */
public final class JsonUtil {

    private static final String UNEXPECTED_TOKEN = "Unexpected token ";

    enum ContextType {
        OBJECT,
        ARRAY
    }

    static final class Context {
        private final ContextType type;
        private String fieldName;

        Context(ContextType type, String fieldName) {
            this.type = Objects.requireNonNull(type);
            this.fieldName = fieldName;
        }

        ContextType getType() {
            return type;
        }

        String getFieldName() {
            return fieldName;
        }

        public void setFieldName(String fieldName) {
            this.fieldName = fieldName;
        }
    }

    private static final Supplier<ExtensionProviders<ExtensionJsonSerializer>> SUPPLIER =
            Suppliers.memoize(() -> ExtensionProviders.createProvider(ExtensionJsonSerializer.class));

    private JsonUtil() {
    }

    public static ObjectMapper createObjectMapper() {
        return JsonMapper.builder()
                .enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING)
                .disable(JsonWriteFeature.WRITE_NAN_AS_STRINGS)
                .enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS)
            .build();
    }

    public static void writeJson(Path jsonFile, Object object, ObjectMapper objectMapper) {
        Objects.requireNonNull(jsonFile);
        Objects.requireNonNull(object);
        Objects.requireNonNull(objectMapper);
        try (Writer writer = Files.newBufferedWriter(jsonFile, StandardCharsets.UTF_8)) {
            objectMapper.writerWithDefaultPrettyPrinter().writeValue(writer, object);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static <T> T readJson(Path jsonFile, Class<T> clazz, ObjectMapper objectMapper) {
        Objects.requireNonNull(jsonFile);
        Objects.requireNonNull(objectMapper);
        try (Reader reader = Files.newBufferedReader(jsonFile, StandardCharsets.UTF_8)) {
            return objectMapper.readValue(reader, clazz);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static <T> T readJsonAndUpdate(InputStream is, T object, ObjectMapper objectMapper) {
        Objects.requireNonNull(is);
        Objects.requireNonNull(object);
        Objects.requireNonNull(objectMapper);
        try {
            return objectMapper.readerForUpdating(object).readValue(is);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static <T> T readJsonAndUpdate(Path jsonFile, T object, ObjectMapper objectMapper) {
        Objects.requireNonNull(jsonFile);
        Objects.requireNonNull(object);
        Objects.requireNonNull(objectMapper);
        try (Reader reader = Files.newBufferedReader(jsonFile, StandardCharsets.UTF_8)) {
            return objectMapper.readerForUpdating(object).readValue(reader);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static JsonFactory createJsonFactory() {
        return new JsonFactoryBuilder()
            .disable(JsonWriteFeature.WRITE_NAN_AS_STRINGS)
            .enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS)
            .build();
    }

    public static void writeJson(Writer writer, Consumer<JsonGenerator> consumer) {
        Objects.requireNonNull(writer);
        Objects.requireNonNull(consumer);
        JsonFactory factory = createJsonFactory();
        try (JsonGenerator generator = factory.createGenerator(writer)) {
            generator.useDefaultPrettyPrinter();
            consumer.accept(generator);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static String toJson(Consumer<JsonGenerator> consumer) {
        try (StringWriter writer = new StringWriter()) {
            writeJson(writer, consumer);
            return writer.toString();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static void writeJson(Path file, Consumer<JsonGenerator> consumer) {
        Objects.requireNonNull(file);
        Objects.requireNonNull(consumer);
        try (BufferedWriter writer = Files.newBufferedWriter(file, StandardCharsets.UTF_8)) {
            writeJson(writer, consumer);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static <T> T parseJson(Path file, Function<JsonParser, T> function) {
        Objects.requireNonNull(file);
        try (BufferedReader reader = Files.newBufferedReader(file, StandardCharsets.UTF_8)) {
            return parseJson(reader, function);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static <T> T parseJson(String json, Function<JsonParser, T> function) {
        Objects.requireNonNull(json);
        try (StringReader reader = new StringReader(json)) {
            return parseJson(reader, function);
        }
    }

    public static <T> T parseJson(Reader reader, Function<JsonParser, T> function) {
        Objects.requireNonNull(reader);
        Objects.requireNonNull(function);
        JsonFactory factory = createJsonFactory();
        try (JsonParser parser = factory.createParser(reader)) {
            return function.apply(parser);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static void writeOptionalStringField(JsonGenerator jsonGenerator, String fieldName, String value) throws IOException {
        Objects.requireNonNull(jsonGenerator);
        Objects.requireNonNull(fieldName);

        if (!Strings.isNullOrEmpty(value)) {
            jsonGenerator.writeStringField(fieldName, value);
        }
    }

    public static void writeOptionalEnumField(JsonGenerator jsonGenerator, String fieldName, Enum<?> value) throws IOException {
        Objects.requireNonNull(jsonGenerator);
        Objects.requireNonNull(fieldName);

        if (value != null) {
            jsonGenerator.writeStringField(fieldName, value.name());
        }
    }

    public static void writeOptionalBooleanField(JsonGenerator jsonGenerator, String fieldName, boolean value, boolean defaultValue) throws IOException {
        Objects.requireNonNull(jsonGenerator);
        Objects.requireNonNull(fieldName);

        if (value != defaultValue) {
            jsonGenerator.writeBooleanField(fieldName, value);
        }
    }

    public static void writeOptionalFloatField(JsonGenerator jsonGenerator, String fieldName, float value) throws IOException {
        Objects.requireNonNull(jsonGenerator);
        Objects.requireNonNull(fieldName);

        if (!Float.isNaN(value)) {
            jsonGenerator.writeNumberField(fieldName, value);
        }
    }

    public static void writeOptionalDoubleField(JsonGenerator jsonGenerator, String fieldName, double value) throws IOException {
        Objects.requireNonNull(jsonGenerator);
        Objects.requireNonNull(fieldName);

        if (!Double.isNaN(value)) {
            jsonGenerator.writeNumberField(fieldName, value);
        }
    }

    public static void writeOptionalDoubleField(JsonGenerator jsonGenerator, String fieldName, double value, double defaultValue) throws IOException {
        Objects.requireNonNull(jsonGenerator);
        Objects.requireNonNull(fieldName);

        if (!Double.isNaN(value) && value != defaultValue) {
            jsonGenerator.writeNumberField(fieldName, value);
        }
    }

    public static void writeOptionalIntegerField(JsonGenerator jsonGenerator, String fieldName, int value) throws IOException {
        Objects.requireNonNull(jsonGenerator);
        Objects.requireNonNull(fieldName);

        if (value != Integer.MAX_VALUE) {
            jsonGenerator.writeNumberField(fieldName, value);
        }
    }

    public static <T> Set<String> writeExtensions(Extendable<T> extendable, JsonGenerator jsonGenerator,
                                                  SerializerProvider serializerProvider) throws IOException {
        return writeExtensions(extendable, jsonGenerator, serializerProvider, SUPPLIER.get());
    }

    public static <T> Set<String> writeExtensions(Extendable<T> extendable, JsonGenerator jsonGenerator,
                                                  SerializerProvider serializerProvider,
                                                  ExtensionProviders<? extends ExtensionJsonSerializer> supplier) throws IOException {
        return writeExtensions(extendable, jsonGenerator, true, serializerProvider, supplier);
    }

    public static <T> Set<String> writeExtensions(Extendable<T> extendable, JsonGenerator jsonGenerator,
                                                  SerializerProvider serializerProvider,
                                                  SerializerSupplier supplier) throws IOException {
        return writeExtensions(extendable, jsonGenerator, true, serializerProvider, supplier);
    }

    public static <T> Set<String> writeExtensions(Extendable<T> extendable, JsonGenerator jsonGenerator,
                                                  boolean headerWanted, SerializerProvider serializerProvider,
                                                  ExtensionProviders<? extends ExtensionJsonSerializer> supplier) throws IOException {
        return writeExtensions(extendable, jsonGenerator, headerWanted, serializerProvider, supplier::findProvider);
    }

    public interface SerializerSupplier {
        ExtensionJsonSerializer getSerializer(String name);
    }

    public static <T> Set<String> writeExtensions(Extendable<T> extendable, JsonGenerator jsonGenerator,
                                                  boolean headerWanted, SerializerProvider serializerProvider,
                                                  SerializerSupplier supplier) throws IOException {
        Objects.requireNonNull(extendable);
        Objects.requireNonNull(jsonGenerator);
        Objects.requireNonNull(serializerProvider);
        Objects.requireNonNull(supplier);

        boolean headerDone = false;
        Set<String> notFound = new HashSet<>();

        if (!extendable.getExtensions().isEmpty()) {
            for (Extension<T> extension : extendable.getExtensions()) {
                ExtensionJsonSerializer serializer = supplier.getSerializer(extension.getName());
                if (serializer != null) {
                    if (!headerDone && headerWanted) {
                        jsonGenerator.writeFieldName("extensions");
                        jsonGenerator.writeStartObject();
                        headerDone = true;
                    }
                    jsonGenerator.writeFieldName(extension.getName());
                    serializer.serialize(extension, jsonGenerator, serializerProvider);
                } else {
                    notFound.add(extension.getName());
                }
            }
            if (headerDone) {
                jsonGenerator.writeEndObject();
            }
        }
        return notFound;
    }

    /**
     * Updates the extensions of the provided extendable with possibly partial definition read from JSON.
     *
     * <p>Note that in order for this to work correctly, extension providers need to implement {@link ExtensionJsonSerializer#deserializeAndUpdate}.
     */
    public static <T extends Extendable> List<Extension<T>> updateExtensions(JsonParser parser, DeserializationContext context, T extendable) throws IOException {
        return updateExtensions(parser, context, SUPPLIER.get(), null, extendable);
    }

    /**
     * Updates the extensions of the provided extendable with possibly partial definition read from JSON.
     *
     * <p>Note that in order for this to work correctly, extension providers need to implement {@link ExtensionJsonSerializer#deserializeAndUpdate}.
     */
    public static <T extends Extendable> List<Extension<T>> updateExtensions(JsonParser parser, DeserializationContext context,
                                                                             ExtensionProviders<? extends ExtensionJsonSerializer> supplier, T extendable) throws IOException {
        return updateExtensions(parser, context, supplier, null, extendable);
    }

    public static <T extends Extendable> List<Extension<T>> updateExtensions(JsonParser parser, DeserializationContext context,
                                                                             SerializerSupplier supplier, T extendable) throws IOException {
        return updateExtensions(parser, context, supplier, null, extendable);
    }

    public static <T extends Extendable> List<Extension<T>> updateExtensions(JsonParser parser, DeserializationContext context,
                                                                             ExtensionProviders<? extends ExtensionJsonSerializer> supplier, Set<String> extensionsNotFound, T extendable) throws IOException {
        return updateExtensions(parser, context, supplier::findProvider, extensionsNotFound, extendable);
    }

    /**
     * Updates the extensions of the provided extendable with possibly partial definition read from JSON.
     *
     * <p>Note that in order for this to work correctly, extension providers need to implement {@link ExtensionJsonSerializer#deserializeAndUpdate}.
     */
    public static <T extends Extendable> List<Extension<T>> updateExtensions(JsonParser parser, DeserializationContext context, SerializerSupplier supplier, Set<String> extensionsNotFound, T extendable) throws IOException {
        Objects.requireNonNull(parser);
        Objects.requireNonNull(context);
        Objects.requireNonNull(supplier);

        List<Extension<T>> extensions = new ArrayList<>();
        if (parser.currentToken() != com.fasterxml.jackson.core.JsonToken.START_OBJECT) {
            throw new PowsyblException("Error updating extensions, \"extensions\" field expected START_OBJECT, got "
                    + parser.currentToken());
        }
        while (parser.nextToken() != JsonToken.END_OBJECT) {
            Extension<T> extension = updateExtension(parser, context, supplier, extensionsNotFound, extendable);
            if (extension != null) {
                extensions.add(extension);
            }
        }
        return extensions;
    }

    private static <T extends Extendable, E extends Extension<T>> E updateExtension(JsonParser parser, DeserializationContext context,
                                                                                    SerializerSupplier supplier, Set<String> extensionsNotFound, T extendable) throws IOException {
        String extensionName = parser.currentName();
        ExtensionJsonSerializer<T, E> extensionJsonSerializer = supplier.getSerializer(extensionName);
        if (extensionJsonSerializer != null) {
            parser.nextToken();
            if (extendable != null && extendable.getExtensionByName(extensionName) != null) {
                return extensionJsonSerializer.deserializeAndUpdate(parser, context, (E) extendable.getExtensionByName(extensionName));
            } else {
                return extensionJsonSerializer.deserialize(parser, context);
            }
        } else {
            if (extensionsNotFound != null) {
                extensionsNotFound.add(extensionName);
            }
            skip(parser);
            return null;
        }
    }

    public static <T extends Extendable> List<Extension<T>> readExtensions(JsonParser parser, DeserializationContext context) throws IOException {
        return readExtensions(parser, context, SUPPLIER.get());
    }

    public static <T extends Extendable> List<Extension<T>> readExtensions(JsonParser parser, DeserializationContext context,
                                                                           ExtensionProviders<? extends ExtensionJsonSerializer> supplier) throws IOException {
        return readExtensions(parser, context, supplier, null);
    }

    public static <T extends Extendable> List<Extension<T>> readExtensions(JsonParser parser, DeserializationContext context,
                                                                           ExtensionProviders<? extends ExtensionJsonSerializer> supplier, Set<String> extensionsNotFound) throws IOException {
        Objects.requireNonNull(parser);
        Objects.requireNonNull(context);
        Objects.requireNonNull(supplier);
        List<Extension<T>> extensions = new ArrayList<>();
        if (parser.currentToken() != JsonToken.START_OBJECT) {
            throw new PowsyblException("Error reading extensions, \"extensions\" field expected START_OBJECT, got "
                    + parser.currentToken());
        }
        while (parser.nextToken() != JsonToken.END_OBJECT) {
            Extension<T> extension = readExtension(parser, context, supplier, extensionsNotFound);
            if (extension != null) {
                extensions.add(extension);
            }
        }
        return extensions;
    }

    public static <T extends Extendable> Extension<T> readExtension(JsonParser parser, DeserializationContext context,
                                                                    ExtensionProviders<? extends ExtensionJsonSerializer> supplier, Set<String> extensionsNotFound) throws IOException {
        Objects.requireNonNull(parser);
        Objects.requireNonNull(context);
        Objects.requireNonNull(supplier);
        return updateExtension(parser, context, supplier::findProvider, extensionsNotFound, null);
    }

    /**
     * Skip a part of a JSON document
     */
    public static void skip(JsonParser parser) throws IOException {
        parser.nextToken();
        parser.skipChildren();
    }

    public static void assertSupportedVersion(String contextName, String version, String maxSupportedVersion) {
        Objects.requireNonNull(version);
        if (version.compareTo(maxSupportedVersion) > 0) {
            String exception = String.format(
                    "%s. Unsupported version %s. Version should be <= %s %n",
                    contextName, version, maxSupportedVersion);
            throw new PowsyblException(exception);
        }
    }

    public static void assertLessThanOrEqualToReferenceVersion(String contextName, String elementName, String version, String referenceVersion) {
        Objects.requireNonNull(version);
        if (version.compareTo(referenceVersion) > 0) {
            String exception = String.format(
                    "%s. %s is not valid for version %s. Version should be <= %s %n",
                    contextName, elementName, version, referenceVersion);
            throw new PowsyblException(exception);
        }
    }

    public static void assertGreaterThanReferenceVersion(String contextName, String elementName, String version, String referenceVersion) {
        Objects.requireNonNull(version);
        if (version.compareTo(referenceVersion) <= 0) {
            String exception = String.format(
                    "%s. %s is not valid for version %s. Version should be > %s %n",
                    contextName, elementName, version, referenceVersion);
            throw new PowsyblException(exception);
        }
    }

    public static void assertGreaterOrEqualThanReferenceVersion(String contextName, String elementName, String version, String referenceVersion) {
        Objects.requireNonNull(version);
        if (version.compareTo(referenceVersion) < 0) {
            String exception = String.format(
                    "%s. %s is not valid for version %s. Version should be >= %s %n",
                    contextName, elementName, version, referenceVersion);
            throw new PowsyblException(exception);
        }
    }

    public static void assertLessThanReferenceVersion(String contextName, String elementName, String version, String referenceVersion) {
        Objects.requireNonNull(version);
        if (version.compareTo(referenceVersion) >= 0) {
            String exception = String.format(
                    "%s. %s is not valid for version %s. Version should be < %s %n",
                    contextName, elementName, version, referenceVersion);
            throw new PowsyblException(exception);
        }
    }

    /**
     * Called by variants of {@link #parseObject} on each encountered field.
     * Should return false if an unexpected field was encountered.
     */
    @FunctionalInterface
    public interface FieldHandler {
        boolean onField(String name) throws IOException;
    }

    /**
     * Parses an object from the current parser position, using the provided field handler.
     * The parsing will expect the starting position to be START_OBJECT.
     */
    public static void parseObject(JsonParser parser, FieldHandler fieldHandler) {
        parseObject(parser, false, fieldHandler);
    }

    /**
     * Parses an object from the current parser position, using the provided field handler.
     * The parsing will accept the starting position to be either a START_OBJECT or a FIELD_NAME,
     * see contract for {@link JsonDeserializer#deserialize(JsonParser, DeserializationContext)}.
     */
    public static void parsePolymorphicObject(JsonParser parser, FieldHandler fieldHandler) {
        parseObject(parser, true, fieldHandler);
    }

    /**
     * Parses an object from the current parser position, using the provided field handler.
     * If {@code polymorphic} is {@code true}, the parsing will accept the starting position
     * to be either a START_OBJECT or a FIELD_NAME, see contract for {@link JsonDeserializer#deserialize(JsonParser, DeserializationContext)}.
     */
    public static void parseObject(JsonParser parser, boolean polymorphic, FieldHandler fieldHandler) {
        Objects.requireNonNull(parser);
        Objects.requireNonNull(fieldHandler);
        try {
            com.fasterxml.jackson.core.JsonToken token = parser.currentToken();
            if (!polymorphic && token != JsonToken.START_OBJECT) {
                throw new PowsyblException("Start object token was expected instead got: " + token);
            }
            if (token == JsonToken.START_OBJECT) {
                token = parser.nextToken();
            }
            while (token != null) {
                if (token == JsonToken.FIELD_NAME) {
                    String fieldName = parser.currentName();
                    boolean found = fieldHandler.onField(fieldName);
                    if (!found) {
                        throw new PowsyblException("Unexpected field " + fieldName);
                    }
                } else if (token == JsonToken.END_OBJECT) {
                    break;
                } else {
                    throw new PowsyblException(UNEXPECTED_TOKEN + token);
                }
                token = parser.nextToken();
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static <T> void parseObjectArray(JsonParser parser, Consumer<T> objectAdder, Function<JsonParser, T> objectParser) {
        Objects.requireNonNull(parser);
        Objects.requireNonNull(objectAdder);
        Objects.requireNonNull(objectParser);
        try {
            JsonToken token = parser.nextToken();
            if (token != JsonToken.START_ARRAY) {
                throw new PowsyblException("Start array token was expected");
            }
            boolean continueLoop = true;
            while (continueLoop && (token = parser.nextToken()) != null) {
                switch (token) {
                    case START_OBJECT -> objectAdder.accept(objectParser.apply(parser));
                    case END_ARRAY -> continueLoop = false;
                    default -> throw new PowsyblException(UNEXPECTED_TOKEN + token);
                }
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @FunctionalInterface
    interface ValueParser<T> {

        T parse(JsonParser parser) throws IOException;
    }

    private static <T> List<T> parseValueArray(JsonParser parser, JsonToken valueToken, ValueParser<T> valueParser) {
        Objects.requireNonNull(parser);
        List<T> values = new ArrayList<>();
        try {
            JsonToken token = parser.nextToken();
            if (token != JsonToken.START_ARRAY) {
                throw new PowsyblException("Start array token was expected");
            }
            while ((token = parser.nextToken()) != null) {
                if (token == valueToken) {
                    values.add(valueParser.parse(parser));
                } else if (token == JsonToken.END_ARRAY) {
                    break;
                } else {
                    throw new PowsyblException(UNEXPECTED_TOKEN + token);
                }
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return values;
    }

    public static List<Integer> parseIntegerArray(JsonParser parser) {
        return parseValueArray(parser, JsonToken.VALUE_NUMBER_INT, JsonParser::getIntValue);
    }

    public static List<Long> parseLongArray(JsonParser parser) {
        return parseValueArray(parser, JsonToken.VALUE_NUMBER_INT, JsonParser::getLongValue);
    }

    public static List<Float> parseFloatArray(JsonParser parser) {
        return parseValueArray(parser, JsonToken.VALUE_NUMBER_FLOAT, JsonParser::getFloatValue);
    }

    public static List<Double> parseDoubleArray(JsonParser parser) {
        return parseValueArray(parser, JsonToken.VALUE_NUMBER_FLOAT, JsonParser::getDoubleValue);
    }

    public static List<String> parseStringArray(JsonParser parser) {
        return parseValueArray(parser, JsonToken.VALUE_STRING, JsonParser::getText);
    }

    /**
     * Saves the provided version into the context (typically a {@link DeserializationContext}),
     * for later retrieval.
     */
    public static void setSourceVersion(DatabindContext context, String version, String sourceVersionAttributeKey) {
        context.setAttribute(sourceVersionAttributeKey, version);
    }

    /**
     * Reads the version from the context (typically a {@link DeserializationContext}) where it has been
     * previously stored.
     */
    public static String getSourceVersion(DatabindContext context, String sourceVersionAttributeKey) {
        return context.getAttribute(sourceVersionAttributeKey) != null ? (String) context.getAttribute(sourceVersionAttributeKey) : null;
    }

    /**
     * Reads a value using the given deserialization context (instead of only using the parser reading method that
     * recreates a context every time).
     * Also handles reading {@code null} values.
     */
    public static <T> T readValue(DeserializationContext context, JsonParser parser, Class<?> type) {
        try {
            if (parser.currentToken() != JsonToken.VALUE_NULL) {
                JavaType jType = context.getTypeFactory()
                        .constructType(type);
                return context.readValue(parser, jType);
            }
            return null;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static <T> List<T> readList(DeserializationContext context, JsonParser parser, Class<?> type) {
        JavaType listType = context.getTypeFactory()
                .constructCollectionType(List.class, type);
        try {
            return context.readValue(parser, listType);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static <T> Set<T> readSet(DeserializationContext context, JsonParser parser, Class<?> type) {
        JavaType setType = context.getTypeFactory()
                .constructCollectionType(Set.class, type);
        try {
            return context.readValue(parser, setType);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public static <T extends Enum> void writeOptionalEnum(JsonGenerator jsonGenerator, String field, Optional<T> optional) throws IOException {
        if (optional.isPresent()) {
            jsonGenerator.writeStringField(field, optional.get().toString());
        }
    }

    public static void writeOptionalDouble(JsonGenerator jsonGenerator, String field, OptionalDouble optional) throws IOException {
        if (optional.isPresent()) {
            jsonGenerator.writeNumberField(field, optional.getAsDouble());
        }
    }

    public static void writeOptionalBoolean(JsonGenerator jsonGenerator, String field, Optional<Boolean> optional) throws IOException {
        if (optional.isPresent()) {
            jsonGenerator.writeBooleanField(field, optional.get());
        }
    }
}