All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.netflix.conductor.common.utils.JsonMapperProvider Maven / Gradle / Ivy

There is a newer version: 3.15.0
Show newest version
package com.netflix.conductor.common.utils;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.google.protobuf.Any;
import com.google.protobuf.ByteString;
import com.google.protobuf.Message;

import javax.inject.Provider;
import java.io.IOException;

public class JsonMapperProvider implements Provider {
    public JsonMapperProvider() {}

    /**
     * JsonProtoModule can be registered into an {@link ObjectMapper}
     * to enable the serialization and deserialization of ProtoBuf objects
     * from/to JSON.
     *
     * Right now this module only provides (de)serialization for the {@link Any}
     * ProtoBuf type, as this is the only ProtoBuf object which we're currently
     * exposing through the REST API.
     *
     * {@see AnySerializer}, {@see AnyDeserializer}
     */
    private static class JsonProtoModule extends SimpleModule {
        private final static String JSON_TYPE = "@type";
        private final static String JSON_VALUE = "@value";

        /**
         * AnySerializer converts a ProtoBuf {@link Any} object into its JSON
         * representation.
         *
         * This is not a canonical ProtoBuf JSON representation. Let us
         * explain what we're trying to accomplish here:
         *
         * The {@link Any} ProtoBuf message is a type in the PB standard library that
         * can store any other arbitrary ProtoBuf message in a type-safe way, even
         * when the server has no knowledge of the schema of the stored message.
         *
         * It accomplishes this by storing a tuple of informtion: an URL-like type
         * declaration for the stored message, and the serialized binary encoding
         * of the stored message itself. Language specific implementations of ProtoBuf
         * provide helper methods to encode and decode arbitrary messages into an
         * {@link Any} object ({@link Any#pack(Message)} in Java).
         *
         * We want to expose these {@link Any} objects in the REST API because they've
         * been introduced as part of the new GRPC interface to Conductor, but unfortunately
         * we cannot encode them using their canonical ProtoBuf JSON encoding. According to
         * the docs:
         *
         *      The JSON representation of an `Any` value uses the regular
         *      representation of the deserialized, embedded message, with an
         *      additional field `@type` which contains the type URL. Example:
         *
         *      package google.profile;
         *      message Person {
         *          string first_name = 1;
         *          string last_name = 2;
         *      }
         *      {
         *          "@type": "type.googleapis.com/google.profile.Person",
         *          "firstName": ,
         *          "lastName": 
         *      }
         *
         * In order to accomplish this representation, the PB-JSON encoder needs to have
         * knowledge of all the ProtoBuf messages that could be serialized inside the
         * {@link Any} message. This is not possible to accomplish inside the Conductor server,
         * which is simply passing through arbitrary payloads from/to clients.
         *
         * Consequently, to actually expose the Message through the REST API, we must create
         * a custom encoding that contains the raw data of the serialized message, as we are
         * not able to deserialize it on the server. We simply return a dictionary with
         * '@type' and '@value' keys, where '@type' is identical to the canonical representation,
         * but '@value' contains a base64 encoded string with the binary data of the serialized
         * message.
         *
         * Since all the provided Conductor clients are required to know this encoding, it's always
         * possible to re-build the original {@link Any} message regardless of the client's language.
         *
         * {@see AnyDeserializer}
         */
        protected class AnySerializer extends JsonSerializer {
            @Override
            public void serialize(Any value, JsonGenerator jgen, SerializerProvider provider)
                    throws IOException, JsonProcessingException {
                jgen.writeStartObject();
                jgen.writeStringField(JSON_TYPE, value.getTypeUrl());
                jgen.writeBinaryField(JSON_VALUE, value.getValue().toByteArray());
                jgen.writeEndObject();
            }
        }

        /**
         * AnyDeserializer converts the custom JSON representation of an {@link Any} value
         * into its original form.
         *
         * {@see AnySerializer} for details on this representation.
         */
        protected class AnyDeserializer extends JsonDeserializer {
            @Override
            public Any deserialize(JsonParser p, DeserializationContext ctxt)
                    throws IOException, JsonProcessingException {
                JsonNode root = p.getCodec().readTree(p);
                JsonNode type = root.get(JSON_TYPE);
                JsonNode value = root.get(JSON_VALUE);

                if (type == null || !type.isTextual()) {
                    throw ctxt.reportMappingException("invalid '@type' field when deserializing ProtoBuf Any object");
                }

                if (value == null || !value.isTextual()) {
                    throw ctxt.reportMappingException("invalid '@value' field when deserializing ProtoBuf Any object");
                }

                return Any.newBuilder()
                        .setTypeUrl(type.textValue())
                        .setValue(ByteString.copyFrom(value.binaryValue()))
                        .build();
            }
        }

        public JsonProtoModule() {
            super("ConductorJsonProtoModule");
            addSerializer(Any.class, new AnySerializer());
            addDeserializer(Any.class, new AnyDeserializer());
        }
    }

    @Override
    public ObjectMapper get() {
        final ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        objectMapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false);
        objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, false);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
        objectMapper.registerModule(new JsonProtoModule());
        return objectMapper;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy