This is an automated email from the ASF dual-hosted git repository.

jihoonson pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git


The following commit(s) were added to refs/heads/master by this push:
     new cc2ffc6  Fix node discovery to ignore unknown DruidServices (#12157)
cc2ffc6 is described below

commit cc2ffc6c0f6f542718e1bb1cdf1063db7cc79021
Author: Jihoon Son <[email protected]>
AuthorDate: Tue Jan 18 22:08:59 2022 -0800

    Fix node discovery to ignore unknown DruidServices (#12157)
    
    * Fix node discovery to ignore unknown DruidServices
    
    * ignore all runtime exceptions
    
    * fix test
    
    * add custom deserializer
    
    * custom serializer
    
    * log host for unparseable druidService
---
 .../druid/client/HttpServerInventoryView.java      |   2 +-
 .../apache/druid/discovery/DataNodeService.java    |  77 ++++-
 .../apache/druid/discovery/DiscoveryDruidNode.java |  93 +++++-
 .../java/org/apache/druid/guice/ServerModule.java  |  22 +-
 .../druid/jackson/DruidServiceSerializer.java      |  89 ++++++
 .../jackson/DruidServiceSerializerModifier.java    |  47 +++
 .../apache/druid/jackson/StringObjectPairList.java |  74 +++++
 .../ToStringObjectPairListDeserializer.java        |  72 +++++
 .../CuratorDruidNodeAnnouncerAndDiscoveryTest.java |   1 +
 .../druid/discovery/DataNodeServiceTest.java       | 101 ++++++-
 .../druid/discovery/DiscoveryDruidNodeTest.java    | 328 +++++++++++++++++++++
 ...ServiceTest.java => DruidServiceTestUtils.java} |  28 +-
 .../druid/discovery/LookupNodeServiceTest.java     |   3 +-
 .../druid/discovery/WorkerNodeServiceTest.java     |   3 +-
 .../StringObjectPairListTest.java}                 |  24 +-
 .../ToStringObjectPairListDeserializerTest.java    |  72 +++++
 .../druid/sql/calcite/schema/SystemSchema.java     |   2 +-
 17 files changed, 970 insertions(+), 68 deletions(-)

diff --git 
a/server/src/main/java/org/apache/druid/client/HttpServerInventoryView.java 
b/server/src/main/java/org/apache/druid/client/HttpServerInventoryView.java
index 9c6f96d..aacd4b7 100644
--- a/server/src/main/java/org/apache/druid/client/HttpServerInventoryView.java
+++ b/server/src/main/java/org/apache/druid/client/HttpServerInventoryView.java
@@ -177,7 +177,7 @@ public class HttpServerInventoryView implements 
ServerInventoryView, FilteredSer
                     node.getDruidNode().getHostAndPort(),
                     node.getDruidNode().getHostAndTlsPort(),
                     ((DataNodeService) 
node.getServices().get(DataNodeService.DISCOVERY_SERVICE_KEY)).getMaxSize(),
-                    ((DataNodeService) 
node.getServices().get(DataNodeService.DISCOVERY_SERVICE_KEY)).getType(),
+                    ((DataNodeService) 
node.getServices().get(DataNodeService.DISCOVERY_SERVICE_KEY)).getServerType(),
                     ((DataNodeService) 
node.getServices().get(DataNodeService.DISCOVERY_SERVICE_KEY)).getTier(),
                     ((DataNodeService) 
node.getServices().get(DataNodeService.DISCOVERY_SERVICE_KEY)).getPriority()
                 );
diff --git 
a/server/src/main/java/org/apache/druid/discovery/DataNodeService.java 
b/server/src/main/java/org/apache/druid/discovery/DataNodeService.java
index 0414684..2f19dc9 100644
--- a/server/src/main/java/org/apache/druid/discovery/DataNodeService.java
+++ b/server/src/main/java/org/apache/druid/discovery/DataNodeService.java
@@ -22,45 +22,94 @@ package org.apache.druid.discovery;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.druid.java.util.common.IAE;
 import org.apache.druid.server.coordination.ServerType;
 
+import javax.annotation.Nullable;
+import java.util.List;
 import java.util.Objects;
 
 /**
  * Metadata announced by any node that serves segments.
+ *
+ * Note for JSON serialization and deserialization.
+ *
+ * This class has a bug that it has the "type" property which is the duplicate 
name
+ * with the subtype key of {@link DruidService}. It seems to happen to work
+ * if the "type" subtype key appears first in the serialized JSON since
+ * Jackson uses the first "type" property as the subtype key.
+ * To always enforce this property order, this class does not use the {@link 
JsonProperty} annotation for serialization,
+ * but uses {@link org.apache.druid.jackson.DruidServiceSerializer}.
+ * Since this is a hacky-way to not break compatibility, a new "serverType" 
field is added
+ * to replace the deprecated "type" field. Once we completely remove the 
"type" field from this class,
+ * we can remove DruidServiceSerializer as well.
+ *
+ * The set of properties to serialize is hard-coded in DruidServiceSerializer.
+ * If you want to add a new field in this class before we remove the "type" 
field,
+ * you must add a proper handling of that new field in DruidServiceSerializer 
as well.
+ *
+ * For deserialization, DruidServices are deserialized as a part of {@link 
DiscoveryDruidNode}.
+ * To handle the bug of the duplicate "type" key, DiscoveryDruidNode first 
deserializes
+ * the JSON to {@link org.apache.druid.jackson.StringObjectPairList},
+ * handles the duplicate "type" keys in the StringObjectPairList,
+ * and then finally converts it to a DruidService. See {@link 
DiscoveryDruidNode#toMap(List)}.
+ *
+ * @see org.apache.druid.jackson.DruidServiceSerializer
+ * @see DiscoveryDruidNode#toMap(List)
  */
 public class DataNodeService extends DruidService
 {
   public static final String DISCOVERY_SERVICE_KEY = "dataNodeService";
+  public static final String SERVER_TYPE_PROP_KEY = "serverType";
 
   private final String tier;
   private final long maxSize;
-  private final ServerType type;
+  private final ServerType serverType;
   private final int priority;
   private final boolean isDiscoverable;
 
+  /**
+   * This JSON creator requires for the "type" subtype key of {@link 
DruidService} to appear before
+   * the "type" property of this class in the serialized JSON. Deserialization 
can fail otherwise.
+   * See the Javadoc of this class for more details.
+   */
   @JsonCreator
-  public DataNodeService(
+  public static DataNodeService fromJson(
       @JsonProperty("tier") String tier,
       @JsonProperty("maxSize") long maxSize,
-      @JsonProperty("type") ServerType type,
+      @JsonProperty("type") @Deprecated @Nullable ServerType type,
+      @JsonProperty(SERVER_TYPE_PROP_KEY) @Nullable ServerType serverType,
       @JsonProperty("priority") int priority
   )
   {
-    this(tier, maxSize, type, priority, true);
+    if (type == null && serverType == null) {
+      throw new IAE("ServerType is missing");
+    }
+    final ServerType theServerType = serverType == null ? type : serverType;
+    return new DataNodeService(tier, maxSize, theServerType, priority);
+  }
+
+  public DataNodeService(
+      String tier,
+      long maxSize,
+      ServerType serverType,
+      int priority
+  )
+  {
+    this(tier, maxSize, serverType, priority, true);
   }
 
   public DataNodeService(
       String tier,
       long maxSize,
-      ServerType type,
+      ServerType serverType,
       int priority,
       boolean isDiscoverable
   )
   {
     this.tier = tier;
     this.maxSize = maxSize;
-    this.type = type;
+    this.serverType = serverType;
     this.priority = priority;
     this.isDiscoverable = isDiscoverable;
   }
@@ -71,30 +120,28 @@ public class DataNodeService extends DruidService
     return DISCOVERY_SERVICE_KEY;
   }
 
-  @JsonProperty
   public String getTier()
   {
     return tier;
   }
 
-  @JsonProperty
   public long getMaxSize()
   {
     return maxSize;
   }
 
-  @JsonProperty
-  public ServerType getType()
+  public ServerType getServerType()
   {
-    return type;
+    return serverType;
   }
 
-  @JsonProperty
   public int getPriority()
   {
     return priority;
   }
 
+  // leaving the "JsonIgnore" annotation to remember that "discoverable" is 
ignored in serialization,
+  // even though the annotation is not actually used.
   @Override
   @JsonIgnore
   public boolean isDiscoverable()
@@ -115,13 +162,13 @@ public class DataNodeService extends DruidService
     return maxSize == that.maxSize &&
            priority == that.priority &&
            Objects.equals(tier, that.tier) &&
-           type == that.type;
+           serverType == that.serverType;
   }
 
   @Override
   public int hashCode()
   {
-    return Objects.hash(tier, maxSize, type, priority);
+    return Objects.hash(tier, maxSize, serverType, priority);
   }
 
   @Override
@@ -130,7 +177,7 @@ public class DataNodeService extends DruidService
     return "DataNodeService{" +
            "tier='" + tier + '\'' +
            ", maxSize=" + maxSize +
-           ", type=" + type +
+           ", serverType=" + serverType +
            ", priority=" + priority +
            '}';
   }
diff --git 
a/server/src/main/java/org/apache/druid/discovery/DiscoveryDruidNode.java 
b/server/src/main/java/org/apache/druid/discovery/DiscoveryDruidNode.java
index ff4ee7f..5b65e74 100644
--- a/server/src/main/java/org/apache/druid/discovery/DiscoveryDruidNode.java
+++ b/server/src/main/java/org/apache/druid/discovery/DiscoveryDruidNode.java
@@ -19,12 +19,21 @@
 
 package org.apache.druid.discovery;
 
+import com.fasterxml.jackson.annotation.JacksonInject;
 import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.Maps;
+import org.apache.druid.jackson.StringObjectPairList;
+import org.apache.druid.java.util.common.IAE;
+import org.apache.druid.java.util.common.NonnullPair;
+import org.apache.druid.java.util.common.logger.Logger;
 import org.apache.druid.server.DruidNode;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Objects;
 
 /**
@@ -35,22 +44,25 @@ import java.util.Objects;
  */
 public class DiscoveryDruidNode
 {
+  private static final Logger LOG = new Logger(DiscoveryDruidNode.class);
+
   private final DruidNode druidNode;
   private final NodeRole nodeRole;
 
   /**
-   * Other metadata associated with the node e.g.
-   * if it's a historical node then lookup information, segment loading 
capacity etc.
+   * Map of service name -> DruidServices.
+   * This map has only the DruidServices that is understandable.
+   * It means, if there is some DruidService not understandable found while 
converting rawServices to services,
+   * that DruidService will be ignored and not stored in this map.
    *
    * @see DruidNodeDiscoveryProvider#SERVICE_TO_NODE_TYPES
    */
   private final Map<String, DruidService> services = new HashMap<>();
 
-  @JsonCreator
   public DiscoveryDruidNode(
-      @JsonProperty("druidNode") DruidNode druidNode,
-      @JsonProperty("nodeType") NodeRole nodeRole,
-      @JsonProperty("services") Map<String, DruidService> services
+      DruidNode druidNode,
+      NodeRole nodeRole,
+      Map<String, DruidService> services
   )
   {
     this.druidNode = druidNode;
@@ -61,6 +73,75 @@ public class DiscoveryDruidNode
     }
   }
 
+  @JsonCreator
+  private static DiscoveryDruidNode fromJson(
+      @JsonProperty("druidNode") DruidNode druidNode,
+      @JsonProperty("nodeType") NodeRole nodeRole,
+      @JsonProperty("services") Map<String, StringObjectPairList> rawServices,
+      @JacksonInject ObjectMapper jsonMapper
+  )
+  {
+    Map<String, DruidService> services = new HashMap<>();
+    if (rawServices != null && !rawServices.isEmpty()) {
+      for (Entry<String, StringObjectPairList> entry : rawServices.entrySet()) 
{
+        List<NonnullPair<String, Object>> val = entry.getValue().getPairs();
+        try {
+          services.put(entry.getKey(), jsonMapper.convertValue(toMap(val), 
DruidService.class));
+        }
+        catch (RuntimeException e) {
+          LOG.warn("Ignore unparseable DruidService for [%s]: %s", 
druidNode.getHostAndPortToUse(), val);
+        }
+      }
+    }
+    return new DiscoveryDruidNode(druidNode, nodeRole, services);
+  }
+
+  /**
+   * A JSON of a {@link DruidService} is deserialized to a Map and then 
converted to aDruidService
+   * to ignore any "unknown" DruidServices to the current node. However, 
directly deserializing a JSON to a Map
+   * is problematic for {@link DataNodeService} as it has duplicate "type" 
keys in its serialized form.
+   * Because of the duplicate key, if we directly deserialize a JSON to a Map, 
we will lose one of the "type" property.
+   * This is definitely a bug of DataNodeService, but, since renaming one of 
those duplicate keys will
+   * break compatibility, DataNodeService still has the deprecated "type" 
property.
+   * See the Javadoc of DataNodeService for more details.
+   *
+   * This function catches such duplicate keys and rewrites the deprecated 
"type" to "serverType",
+   * so that we don't lose any properties.
+   *
+   * This method can be removed together when we entirely remove the 
deprecated "type" property from DataNodeService.
+   */
+  @Deprecated
+  private static Map<String, Object> toMap(List<NonnullPair<String, Object>> 
pairs)
+  {
+    final Map<String, Object> map = 
Maps.newHashMapWithExpectedSize(pairs.size());
+    for (NonnullPair<String, Object> pair : pairs) {
+      final Object prevVal = map.put(pair.lhs, pair.rhs);
+      if (prevVal != null) {
+        if ("type".equals(pair.lhs)) {
+          if (DataNodeService.DISCOVERY_SERVICE_KEY.equals(prevVal)) {
+            map.put("type", prevVal);
+            // this one is likely serverType.
+            map.put(DataNodeService.SERVER_TYPE_PROP_KEY, pair.rhs);
+            continue;
+          } else if (DataNodeService.DISCOVERY_SERVICE_KEY.equals(pair.rhs)) {
+            // this one is likely serverType.
+            map.put(DataNodeService.SERVER_TYPE_PROP_KEY, prevVal);
+            continue;
+          }
+        } else if (DataNodeService.SERVER_TYPE_PROP_KEY.equals(pair.lhs)) {
+          // Ignore duplicate "serverType" keys since it can happen
+          // when the JSON has both "type" and "serverType" keys for 
serverType.
+          continue;
+        }
+
+        if (!prevVal.equals(pair.rhs)) {
+          throw new IAE("Duplicate key[%s] with different values: [%s] and 
[%s]", pair.lhs, prevVal, pair.rhs);
+        }
+      }
+    }
+    return map;
+  }
+
   @JsonProperty
   public Map<String, DruidService> getServices()
   {
diff --git a/server/src/main/java/org/apache/druid/guice/ServerModule.java 
b/server/src/main/java/org/apache/druid/guice/ServerModule.java
index 503e71a..7ec37a0 100644
--- a/server/src/main/java/org/apache/druid/guice/ServerModule.java
+++ b/server/src/main/java/org/apache/druid/guice/ServerModule.java
@@ -19,19 +19,27 @@
 
 package org.apache.druid.guice;
 
+import com.fasterxml.jackson.databind.Module;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.google.common.collect.ImmutableList;
 import com.google.inject.Binder;
-import com.google.inject.Module;
 import com.google.inject.Provides;
 import org.apache.druid.guice.annotations.Self;
+import org.apache.druid.initialization.DruidModule;
+import org.apache.druid.jackson.DruidServiceSerializerModifier;
+import org.apache.druid.jackson.StringObjectPairList;
+import org.apache.druid.jackson.ToStringObjectPairListDeserializer;
 import org.apache.druid.java.util.common.concurrent.ScheduledExecutorFactory;
 import org.apache.druid.java.util.common.concurrent.ScheduledExecutors;
 import org.apache.druid.java.util.common.lifecycle.Lifecycle;
 import org.apache.druid.server.DruidNode;
 import org.apache.druid.server.initialization.ZkPathsConfig;
 
+import java.util.List;
+
 /**
  */
-public class ServerModule implements Module
+public class ServerModule implements DruidModule
 {
   public static final String ZK_PATHS_PROPERTY_BASE = "druid.zk.paths";
 
@@ -47,4 +55,14 @@ public class ServerModule implements Module
   {
     return ScheduledExecutors.createFactory(lifecycle);
   }
+
+  @Override
+  public List<? extends Module> getJacksonModules()
+  {
+    return ImmutableList.of(
+        new SimpleModule()
+            .addDeserializer(StringObjectPairList.class, new 
ToStringObjectPairListDeserializer())
+            .setSerializerModifier(new DruidServiceSerializerModifier())
+    );
+  }
 }
diff --git 
a/server/src/main/java/org/apache/druid/jackson/DruidServiceSerializer.java 
b/server/src/main/java/org/apache/druid/jackson/DruidServiceSerializer.java
new file mode 100644
index 0000000..cb64da8
--- /dev/null
+++ b/server/src/main/java/org/apache/druid/jackson/DruidServiceSerializer.java
@@ -0,0 +1,89 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.jackson;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+import org.apache.druid.discovery.DataNodeService;
+import org.apache.druid.discovery.DruidService;
+
+import java.io.IOException;
+
+/**
+ * A custom serializer to handle the bug of duplicate "type" keys in {@link 
DataNodeService}.
+ * This class can be removed together when we entirely remove the deprecated 
"type" property from DataNodeService.
+ * See the Javadoc of DataNodeService for more details.
+ */
+public class DruidServiceSerializer extends StdSerializer<DruidService>
+{
+  private final JsonSerializer<Object> defaultSerializer;
+
+  public DruidServiceSerializer(JsonSerializer<Object> defaultSerializer)
+  {
+    super(DruidService.class);
+    this.defaultSerializer = defaultSerializer;
+  }
+
+  @Override
+  public void serialize(DruidService druidService, JsonGenerator gen, 
SerializerProvider serializers) throws IOException
+  {
+    defaultSerializer.serialize(druidService, gen, serializers);
+  }
+
+  @Override
+  public void serializeWithType(
+      DruidService druidService,
+      JsonGenerator gen,
+      SerializerProvider serializers,
+      TypeSerializer typeSer
+  ) throws IOException
+  {
+    if (druidService instanceof DataNodeService) {
+      DataNodeService dataNodeService = (DataNodeService) druidService;
+      gen.writeStartObject();
+
+      // Write subtype key first. This is important because Jackson picks up 
the first "type" field as the subtype key
+      // for deserialization.
+      gen.writeStringField("type", DataNodeService.DISCOVERY_SERVICE_KEY);
+
+      // Write properties of DataNodeService
+      gen.writeStringField("tier", dataNodeService.getTier());
+      gen.writeNumberField("maxSize", dataNodeService.getMaxSize());
+      // NOTE: the below line writes a duplicate key of "type".
+      // This is a bug that DataNodeService has a key of the same name as the 
subtype key.
+      // To address the bug, a new "serverType" field has been added.
+      // However, we cannot remove the deprecated "type" entirely yet because 
it will break rolling upgrade.
+      // It seems OK to have duplicate keys though because Jackson seems to 
always pick up the first "type" property
+      // as the subtype key for deserialization.
+      // This duplicate key should be removed in a future release.
+      // See DiscoveryDruidNode.toMap() for deserialization of DruidServices.
+      gen.writeObjectField("type", dataNodeService.getServerType());
+      gen.writeObjectField("serverType", dataNodeService.getServerType());
+      gen.writeNumberField("priority", dataNodeService.getPriority());
+
+      gen.writeEndObject();
+    } else {
+      defaultSerializer.serializeWithType(druidService, gen, serializers, 
typeSer);
+    }
+  }
+}
diff --git 
a/server/src/main/java/org/apache/druid/jackson/DruidServiceSerializerModifier.java
 
b/server/src/main/java/org/apache/druid/jackson/DruidServiceSerializerModifier.java
new file mode 100644
index 0000000..f16e139
--- /dev/null
+++ 
b/server/src/main/java/org/apache/druid/jackson/DruidServiceSerializerModifier.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.jackson;
+
+import com.fasterxml.jackson.databind.BeanDescription;
+import com.fasterxml.jackson.databind.JsonSerializer;
+import com.fasterxml.jackson.databind.SerializationConfig;
+import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
+import org.apache.druid.discovery.DruidService;
+
+/**
+ * A modifier to use a custom serializer for {@link DruidService}.
+ * See {@link DruidServiceSerializer} for details.
+ */
+public class DruidServiceSerializerModifier extends BeanSerializerModifier
+{
+  @Override
+  public JsonSerializer<?> modifySerializer(
+      SerializationConfig config,
+      BeanDescription beanDesc,
+      JsonSerializer<?> serializer
+  )
+  {
+    if (DruidService.class.isAssignableFrom(beanDesc.getBeanClass())) {
+      return new DruidServiceSerializer((JsonSerializer<Object>) serializer);
+    }
+
+    return serializer;
+  }
+}
diff --git 
a/server/src/main/java/org/apache/druid/jackson/StringObjectPairList.java 
b/server/src/main/java/org/apache/druid/jackson/StringObjectPairList.java
new file mode 100644
index 0000000..db5ab4b
--- /dev/null
+++ b/server/src/main/java/org/apache/druid/jackson/StringObjectPairList.java
@@ -0,0 +1,74 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.jackson;
+
+import org.apache.druid.discovery.DiscoveryDruidNode;
+import org.apache.druid.java.util.common.NonnullPair;
+
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * When {@link DiscoveryDruidNode} is deserialized from a JSON, the JSON is 
first converted to this class,
+ * and then to a Map. See {@link DiscoveryDruidNode#toMap} for details.
+ *
+ * @see ToStringObjectPairListDeserializer
+ */
+public class StringObjectPairList
+{
+  private final List<NonnullPair<String, Object>> pairs;
+
+  public StringObjectPairList(List<NonnullPair<String, Object>> pairs)
+  {
+    this.pairs = pairs;
+  }
+
+  public List<NonnullPair<String, Object>> getPairs()
+  {
+    return pairs;
+  }
+
+  @Override
+  public boolean equals(Object o)
+  {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    StringObjectPairList that = (StringObjectPairList) o;
+    return Objects.equals(pairs, that.pairs);
+  }
+
+  @Override
+  public int hashCode()
+  {
+    return Objects.hash(pairs);
+  }
+
+  @Override
+  public String toString()
+  {
+    return "StringObjectPairList{" +
+           "pairs=" + pairs +
+           '}';
+  }
+}
diff --git 
a/server/src/main/java/org/apache/druid/jackson/ToStringObjectPairListDeserializer.java
 
b/server/src/main/java/org/apache/druid/jackson/ToStringObjectPairListDeserializer.java
new file mode 100644
index 0000000..d517b9f
--- /dev/null
+++ 
b/server/src/main/java/org/apache/druid/jackson/ToStringObjectPairListDeserializer.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.jackson;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.core.JsonToken;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+import org.apache.druid.discovery.DiscoveryDruidNode;
+import org.apache.druid.discovery.DruidService;
+import org.apache.druid.java.util.common.NonnullPair;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * When {@link DiscoveryDruidNode} is deserialized from a JSON,
+ * the JSON is first converted to {@link StringObjectPairList}, and then to a 
Map.
+ * See {@link DiscoveryDruidNode#toMap} for details.
+ */
+public class ToStringObjectPairListDeserializer extends 
StdDeserializer<StringObjectPairList>
+{
+  public ToStringObjectPairListDeserializer()
+  {
+    super(StringObjectPairList.class);
+  }
+
+  @Override
+  public StringObjectPairList deserialize(JsonParser parser, 
DeserializationContext ctx) throws IOException
+  {
+    if (parser.currentToken() != JsonToken.START_OBJECT) {
+      throw ctx.wrongTokenException(parser, DruidService.class, 
JsonToken.START_OBJECT, null);
+    }
+
+    final List<NonnullPair<String, Object>> pairs = new ArrayList<>();
+
+    parser.nextToken();
+
+    while (parser.currentToken() == JsonToken.FIELD_NAME) {
+      final String key = parser.getText();
+      parser.nextToken();
+      final Object val = ctx.readValue(parser, Object.class);
+      pairs.add(new NonnullPair<>(key, val));
+
+      parser.nextToken();
+    }
+
+    if (parser.currentToken() != JsonToken.END_OBJECT) {
+      throw ctx.wrongTokenException(parser, DruidService.class, 
JsonToken.END_OBJECT, null);
+    }
+
+    return new StringObjectPairList(pairs);
+  }
+}
diff --git 
a/server/src/test/java/org/apache/druid/curator/discovery/CuratorDruidNodeAnnouncerAndDiscoveryTest.java
 
b/server/src/test/java/org/apache/druid/curator/discovery/CuratorDruidNodeAnnouncerAndDiscoveryTest.java
index 1851cc7..9346687 100644
--- 
a/server/src/test/java/org/apache/druid/curator/discovery/CuratorDruidNodeAnnouncerAndDiscoveryTest.java
+++ 
b/server/src/test/java/org/apache/druid/curator/discovery/CuratorDruidNodeAnnouncerAndDiscoveryTest.java
@@ -64,6 +64,7 @@ public class CuratorDruidNodeAnnouncerAndDiscoveryTest 
extends CuratorTestBase
             .addValue(ServerConfig.class, new ServerConfig())
             .addValue("java.lang.String", "dummy")
             .addValue("java.lang.Integer", 1234)
+            .addValue(ObjectMapper.class, objectMapper)
     );
 
     curator.start();
diff --git 
a/server/src/test/java/org/apache/druid/discovery/DataNodeServiceTest.java 
b/server/src/test/java/org/apache/druid/discovery/DataNodeServiceTest.java
index 99325f6..82c978c 100644
--- a/server/src/test/java/org/apache/druid/discovery/DataNodeServiceTest.java
+++ b/server/src/test/java/org/apache/druid/discovery/DataNodeServiceTest.java
@@ -19,8 +19,9 @@
 
 package org.apache.druid.discovery;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.druid.segment.TestHelper;
+import org.apache.druid.java.util.common.StringUtils;
 import org.apache.druid.server.coordination.ServerType;
 import org.junit.Assert;
 import org.junit.Test;
@@ -29,6 +30,8 @@ import org.junit.Test;
  */
 public class DataNodeServiceTest
 {
+  private final ObjectMapper mapper = DruidServiceTestUtils.newJsonMapper();
+
   @Test
   public void testSerde() throws Exception
   {
@@ -39,7 +42,6 @@ public class DataNodeServiceTest
         1
     );
 
-    ObjectMapper mapper = TestHelper.makeJsonMapper();
     DruidService actual = mapper.readValue(
         mapper.writeValueAsString(expected),
         DruidService.class
@@ -47,4 +49,99 @@ public class DataNodeServiceTest
 
     Assert.assertEquals(expected, actual);
   }
+
+  @Test
+  public void testDeserializeWithDeprecatedServerTypeProperty() throws 
Exception
+  {
+    String json = "{\n"
+                  + "  \"type\" : \"dataNodeService\",\n"
+                  + "  \"tier\" : \"tier\",\n"
+                  + "  \"maxSize\" : 100,\n"
+                  + "  \"type\" : \"historical\",\n"
+                  + "  \"priority\" : 1\n"
+                  + "}";
+
+    DruidService actual = mapper.readValue(
+        json,
+        DruidService.class
+    );
+
+    Assert.assertEquals(
+        new DataNodeService(
+            "tier",
+            100,
+            ServerType.HISTORICAL,
+            1
+        ),
+        actual
+    );
+  }
+
+  @Test
+  public void testDeserializeWithServerTypeProperty() throws Exception
+  {
+    String json = "{\n"
+                  + "  \"type\" : \"dataNodeService\",\n"
+                  + "  \"tier\" : \"tier\",\n"
+                  + "  \"maxSize\" : 100,\n"
+                  + "  \"serverType\" : \"historical\",\n"
+                  + "  \"priority\" : 1\n"
+                  + "}";
+
+    DruidService actual = mapper.readValue(
+        json,
+        DruidService.class
+    );
+
+    Assert.assertEquals(
+        new DataNodeService(
+            "tier",
+            100,
+            ServerType.HISTORICAL,
+            1
+        ),
+        actual
+    );
+  }
+
+  @Test
+  public void testSerdeDeserializeWithBothDeprecatedAndNewServerTypes() throws 
Exception
+  {
+    String json = "{\n"
+                  + "  \"type\" : \"dataNodeService\",\n"
+                  + "  \"tier\" : \"tier\",\n"
+                  + "  \"maxSize\" : 100,\n"
+                  + "  \"type\" : \"historical\",\n"
+                  + "  \"serverType\" : \"historical\",\n"
+                  + "  \"priority\" : 1\n"
+                  + "}";
+
+    DruidService actual = mapper.readValue(
+        json,
+        DruidService.class
+    );
+
+    Assert.assertEquals(
+        new DataNodeService(
+            "tier",
+            100,
+            ServerType.HISTORICAL,
+            1
+        ),
+        actual
+    );
+  }
+
+  @Test
+  public void testSerializeSubtypeKeyShouldAppearFirstInJson() throws 
JsonProcessingException
+  {
+    final DataNodeService dataNodeService = new DataNodeService(
+        "tier",
+        100,
+        ServerType.HISTORICAL,
+        1
+    );
+    final String json = mapper.writeValueAsString(dataNodeService);
+    Assert.assertTrue(json.startsWith(StringUtils.format("{\"type\":\"%s\"", 
DataNodeService.DISCOVERY_SERVICE_KEY)));
+  }
 }
diff --git 
a/server/src/test/java/org/apache/druid/discovery/DiscoveryDruidNodeTest.java 
b/server/src/test/java/org/apache/druid/discovery/DiscoveryDruidNodeTest.java
new file mode 100644
index 0000000..64976bd
--- /dev/null
+++ 
b/server/src/test/java/org/apache/druid/discovery/DiscoveryDruidNodeTest.java
@@ -0,0 +1,328 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.discovery;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.InjectableValues.Std;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import nl.jqno.equalsverifier.EqualsVerifier;
+import org.apache.druid.guice.ServerModule;
+import org.apache.druid.jackson.DefaultObjectMapper;
+import org.apache.druid.server.DruidNode;
+import org.apache.druid.server.coordination.ServerType;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Collection;
+
+public class DiscoveryDruidNodeTest
+{
+  private final DruidNode druidNode;
+
+  private final NodeRole nodeRole;
+
+  public DiscoveryDruidNodeTest()
+  {
+    this.druidNode = new DruidNode(
+        "testNode",
+        "host",
+        true,
+        8082,
+        null,
+        true,
+        false
+    );
+    nodeRole = NodeRole.BROKER;
+  }
+
+  @Test
+  public void testEquals()
+  {
+    EqualsVerifier.forClass(DiscoveryDruidNode.class)
+                  .withNonnullFields("druidNode", "nodeRole", "services")
+                  .usingGetClass()
+                  .verify();
+  }
+
+  @Test
+  public void testDeserialize() throws JsonProcessingException
+  {
+    final ObjectMapper mapper = 
createObjectMapper(ImmutableList.of(Service1.class, Service2.class));
+    final DiscoveryDruidNode node = new DiscoveryDruidNode(
+        druidNode,
+        nodeRole,
+        ImmutableMap.of("service1", new Service1(), "service2", new Service2())
+    );
+    final String json = mapper.writeValueAsString(node);
+    final DiscoveryDruidNode fromJson = mapper.readValue(json, 
DiscoveryDruidNode.class);
+    Assert.assertEquals(node, fromJson);
+  }
+
+  @Test
+  public void testDeserializeIgnorUnknownDruidService() throws 
JsonProcessingException
+  {
+    final ObjectMapper mapper = 
createObjectMapper(ImmutableList.of(Service1.class));
+    final DiscoveryDruidNode node = new DiscoveryDruidNode(
+        druidNode,
+        nodeRole,
+        ImmutableMap.of("service1", new Service1(), "service2", new Service2())
+    );
+    final String json = mapper.writeValueAsString(node);
+    final DiscoveryDruidNode fromJson = mapper.readValue(json, 
DiscoveryDruidNode.class);
+    Assert.assertEquals(
+        new DiscoveryDruidNode(
+            druidNode,
+            nodeRole,
+            ImmutableMap.of("service1", new Service1())
+        ),
+        fromJson
+    );
+  }
+
+  @Test
+  public void testSerdeWithDataNodeAndLookupNodeServices() throws 
JsonProcessingException
+  {
+    final ObjectMapper mapper = createObjectMapper(ImmutableList.of());
+    final DiscoveryDruidNode node = new DiscoveryDruidNode(
+        new DruidNode(
+            "druid/broker",
+            "druid-broker",
+            false,
+            8082,
+            -1,
+            8282,
+            true,
+            true
+        ),
+        NodeRole.BROKER,
+        ImmutableMap.of(
+            DataNodeService.DISCOVERY_SERVICE_KEY,
+            new DataNodeService("_default_tier", 1000000000, 
ServerType.BROKER, 0),
+            LookupNodeService.DISCOVERY_SERVICE_KEY,
+            new LookupNodeService("lookup_tier")
+        )
+    );
+    final String json = mapper.writeValueAsString(node);
+    Assert.assertEquals(
+        node,
+        mapper.readValue(json, DiscoveryDruidNode.class)
+    );
+  }
+
+  @Test
+  public void testDeserializeWithDataNodeServiceWithAWrongPropertyOrder() 
throws JsonProcessingException
+  {
+    final ObjectMapper mapper = createObjectMapper(ImmutableList.of());
+    final String json = "{\n"
+                        + "  \"druidNode\" : {\n"
+                        + "    \"service\" : \"druid/broker\",\n"
+                        + "    \"host\" : \"druid-broker\",\n"
+                        + "    \"bindOnHost\" : false,\n"
+                        + "    \"plaintextPort\" : 8082,\n"
+                        + "    \"port\" : -1,\n"
+                        + "    \"tlsPort\" : 8282,\n"
+                        + "    \"enablePlaintextPort\" : true,\n"
+                        + "    \"enableTlsPort\" : true\n"
+                        + "  },\n"
+                        + "  \"nodeType\" : \"broker\",\n"
+                        + "  \"services\" : {\n"
+                        + "    \"dataNodeService\" : {\n"
+                        // In normal case, this proprty must appear after 
another "type" below.
+                        + "      \"type\" : \"broker\",\n"
+                        + "      \"type\" : \"dataNodeService\",\n"
+                        + "      \"tier\" : \"_default_tier\",\n"
+                        + "      \"maxSize\" : 1000000000,\n"
+                        + "      \"serverType\" : \"broker\",\n"
+                        + "      \"priority\" : 0\n"
+                        + "    }\n"
+                        + "  }\n"
+                        + "}";
+    Assert.assertEquals(
+        new DiscoveryDruidNode(
+            new DruidNode(
+                "druid/broker",
+                "druid-broker",
+                false,
+                8082,
+                -1,
+                8282,
+                true,
+                true
+            ),
+            NodeRole.BROKER,
+            ImmutableMap.of(
+                "dataNodeService",
+                new DataNodeService("_default_tier", 1000000000, 
ServerType.BROKER, 0)
+            )
+        ),
+        mapper.readValue(json, DiscoveryDruidNode.class)
+    );
+  }
+
+  @Test
+  public void testDeserialize_duplicateProperties_shouldSucceedToDeserialize() 
throws JsonProcessingException
+  {
+    final ObjectMapper mapper = createObjectMapper(ImmutableList.of());
+    final String json = "{\n"
+                        + "  \"druidNode\" : {\n"
+                        + "    \"service\" : \"druid/broker\",\n"
+                        + "    \"host\" : \"druid-broker\",\n"
+                        + "    \"bindOnHost\" : false,\n"
+                        + "    \"plaintextPort\" : 8082,\n"
+                        + "    \"port\" : -1,\n"
+                        + "    \"tlsPort\" : 8282,\n"
+                        + "    \"enablePlaintextPort\" : true,\n"
+                        + "    \"enableTlsPort\" : true\n"
+                        + "  },\n"
+                        + "  \"nodeType\" : \"broker\",\n"
+                        + "  \"services\" : {\n"
+                        + "    \"dataNodeService\" : {\n"
+                        + "      \"type\" : \"dataNodeService\",\n"
+                        + "      \"tier\" : \"_default_tier\",\n"
+                        + "      \"maxSize\" : 1000000000,\n"
+                        + "      \"maxSize\" : 1000000000,\n"
+                        + "      \"serverType\" : \"broker\",\n"
+                        + "      \"priority\" : 0\n"
+                        + "    }\n"
+                        + "  }\n"
+                        + "}";
+    Assert.assertEquals(
+        new DiscoveryDruidNode(
+            new DruidNode(
+                "druid/broker",
+                "druid-broker",
+                false,
+                8082,
+                -1,
+                8282,
+                true,
+                true
+            ),
+            NodeRole.BROKER,
+            ImmutableMap.of(
+                "dataNodeService",
+                new DataNodeService("_default_tier", 1000000000, 
ServerType.BROKER, 0)
+            )
+        ),
+        mapper.readValue(json, DiscoveryDruidNode.class)
+    );
+  }
+
+  @Test
+  public void 
testDeserialize_duplicateKeysWithDifferentValus_shouldIgnoreDataNodeService()
+      throws JsonProcessingException
+  {
+    final ObjectMapper mapper = createObjectMapper(ImmutableList.of());
+    final String json = "{\n"
+                        + "  \"druidNode\" : {\n"
+                        + "    \"service\" : \"druid/broker\",\n"
+                        + "    \"host\" : \"druid-broker\",\n"
+                        + "    \"bindOnHost\" : false,\n"
+                        + "    \"plaintextPort\" : 8082,\n"
+                        + "    \"port\" : -1,\n"
+                        + "    \"tlsPort\" : 8282,\n"
+                        + "    \"enablePlaintextPort\" : true,\n"
+                        + "    \"enableTlsPort\" : true\n"
+                        + "  },\n"
+                        + "  \"nodeType\" : \"broker\",\n"
+                        + "  \"services\" : {\n"
+                        + "    \"dataNodeService\" : {\n"
+                        + "      \"type\" : \"dataNodeService\",\n"
+                        + "      \"tier\" : \"_default_tier\",\n"
+                        + "      \"maxSize\" : 1000000000,\n"
+                        + "      \"maxSize\" : 10,\n"
+                        + "      \"serverType\" : \"broker\",\n"
+                        + "      \"priority\" : 0\n"
+                        + "    }\n"
+                        + "  }\n"
+                        + "}";
+    Assert.assertEquals(
+        new DiscoveryDruidNode(
+            new DruidNode(
+                "druid/broker",
+                "druid-broker",
+                false,
+                8082,
+                -1,
+                8282,
+                true,
+                true
+            ),
+            NodeRole.BROKER,
+            ImmutableMap.of()
+        ),
+        mapper.readValue(json, DiscoveryDruidNode.class)
+    );
+  }
+
+  private static class Service1 extends DruidService
+  {
+    @Override
+    public String getName()
+    {
+      return "service1";
+    }
+
+    @Override
+    public int hashCode()
+    {
+      return 0;
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+      return obj instanceof Service1;
+    }
+  }
+
+  private static class Service2 extends DruidService
+  {
+    @Override
+    public String getName()
+    {
+      return "service2";
+    }
+
+    @Override
+    public int hashCode()
+    {
+      return 0;
+    }
+
+    @Override
+    public boolean equals(Object obj)
+    {
+      return obj instanceof Service2;
+    }
+  }
+
+  private static ObjectMapper createObjectMapper(Collection<Class<? extends 
DruidService>> druidServicesToRegister)
+  {
+    final ObjectMapper mapper = new DefaultObjectMapper();
+    mapper.registerModules(new ServerModule().getJacksonModules());
+    //noinspection unchecked,rawtypes
+    mapper.registerSubtypes((Collection) druidServicesToRegister);
+    mapper.setInjectableValues(new Std().addValue(ObjectMapper.class, mapper));
+    return mapper;
+  }
+}
diff --git 
a/server/src/test/java/org/apache/druid/discovery/LookupNodeServiceTest.java 
b/server/src/test/java/org/apache/druid/discovery/DruidServiceTestUtils.java
similarity index 65%
copy from 
server/src/test/java/org/apache/druid/discovery/LookupNodeServiceTest.java
copy to 
server/src/test/java/org/apache/druid/discovery/DruidServiceTestUtils.java
index fb1c3d7..c8e572a 100644
--- a/server/src/test/java/org/apache/druid/discovery/LookupNodeServiceTest.java
+++ b/server/src/test/java/org/apache/druid/discovery/DruidServiceTestUtils.java
@@ -20,27 +20,19 @@
 package org.apache.druid.discovery;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.druid.segment.TestHelper;
-import org.junit.Assert;
-import org.junit.Test;
+import org.apache.druid.guice.ServerModule;
+import org.apache.druid.jackson.DefaultObjectMapper;
 
-/**
- */
-public class LookupNodeServiceTest
+public final class DruidServiceTestUtils
 {
-  @Test
-  public void testSerde() throws Exception
+  public static ObjectMapper newJsonMapper()
   {
-    DruidService expected = new LookupNodeService(
-        "tier"
-    );
-
-    ObjectMapper mapper = TestHelper.makeJsonMapper();
-    DruidService actual = mapper.readValue(
-        mapper.writeValueAsString(expected),
-        DruidService.class
-    );
+    final ObjectMapper mapper = new DefaultObjectMapper();
+    mapper.registerModules(new ServerModule().getJacksonModules());
+    return mapper;
+  }
 
-    Assert.assertEquals(expected, actual);
+  private DruidServiceTestUtils()
+  {
   }
 }
diff --git 
a/server/src/test/java/org/apache/druid/discovery/LookupNodeServiceTest.java 
b/server/src/test/java/org/apache/druid/discovery/LookupNodeServiceTest.java
index fb1c3d7..38e5085 100644
--- a/server/src/test/java/org/apache/druid/discovery/LookupNodeServiceTest.java
+++ b/server/src/test/java/org/apache/druid/discovery/LookupNodeServiceTest.java
@@ -20,7 +20,6 @@
 package org.apache.druid.discovery;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.druid.segment.TestHelper;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -35,7 +34,7 @@ public class LookupNodeServiceTest
         "tier"
     );
 
-    ObjectMapper mapper = TestHelper.makeJsonMapper();
+    ObjectMapper mapper = DruidServiceTestUtils.newJsonMapper();
     DruidService actual = mapper.readValue(
         mapper.writeValueAsString(expected),
         DruidService.class
diff --git 
a/server/src/test/java/org/apache/druid/discovery/WorkerNodeServiceTest.java 
b/server/src/test/java/org/apache/druid/discovery/WorkerNodeServiceTest.java
index b4a08e9..2f468ee 100644
--- a/server/src/test/java/org/apache/druid/discovery/WorkerNodeServiceTest.java
+++ b/server/src/test/java/org/apache/druid/discovery/WorkerNodeServiceTest.java
@@ -20,7 +20,6 @@
 package org.apache.druid.discovery;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.druid.segment.TestHelper;
 import org.junit.Assert;
 import org.junit.Test;
 
@@ -38,7 +37,7 @@ public class WorkerNodeServiceTest
         "c1"
     );
 
-    ObjectMapper mapper = TestHelper.makeJsonMapper();
+    ObjectMapper mapper = DruidServiceTestUtils.newJsonMapper();
     DruidService actual = mapper.readValue(
         mapper.writeValueAsString(expected),
         DruidService.class
diff --git 
a/server/src/test/java/org/apache/druid/discovery/LookupNodeServiceTest.java 
b/server/src/test/java/org/apache/druid/jackson/StringObjectPairListTest.java
similarity index 61%
copy from 
server/src/test/java/org/apache/druid/discovery/LookupNodeServiceTest.java
copy to 
server/src/test/java/org/apache/druid/jackson/StringObjectPairListTest.java
index fb1c3d7..4905577 100644
--- a/server/src/test/java/org/apache/druid/discovery/LookupNodeServiceTest.java
+++ 
b/server/src/test/java/org/apache/druid/jackson/StringObjectPairListTest.java
@@ -17,30 +17,16 @@
  * under the License.
  */
 
-package org.apache.druid.discovery;
+package org.apache.druid.jackson;
 
-import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.druid.segment.TestHelper;
-import org.junit.Assert;
+import nl.jqno.equalsverifier.EqualsVerifier;
 import org.junit.Test;
 
-/**
- */
-public class LookupNodeServiceTest
+public class StringObjectPairListTest
 {
   @Test
-  public void testSerde() throws Exception
+  public void testEquals()
   {
-    DruidService expected = new LookupNodeService(
-        "tier"
-    );
-
-    ObjectMapper mapper = TestHelper.makeJsonMapper();
-    DruidService actual = mapper.readValue(
-        mapper.writeValueAsString(expected),
-        DruidService.class
-    );
-
-    Assert.assertEquals(expected, actual);
+    
EqualsVerifier.forClass(StringObjectPairList.class).usingGetClass().withNonnullFields("pairs").verify();
   }
 }
diff --git 
a/server/src/test/java/org/apache/druid/jackson/ToStringObjectPairListDeserializerTest.java
 
b/server/src/test/java/org/apache/druid/jackson/ToStringObjectPairListDeserializerTest.java
new file mode 100644
index 0000000..5622dc4
--- /dev/null
+++ 
b/server/src/test/java/org/apache/druid/jackson/ToStringObjectPairListDeserializerTest.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.druid.jackson;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.module.SimpleModule;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import org.apache.druid.java.util.common.NonnullPair;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.Map;
+
+public class ToStringObjectPairListDeserializerTest
+{
+  private final ObjectMapper objectMapper;
+
+  public ToStringObjectPairListDeserializerTest()
+  {
+    objectMapper = new DefaultObjectMapper();
+    objectMapper.registerModule(
+        new SimpleModule().addDeserializer(
+            StringObjectPairList.class,
+            new ToStringObjectPairListDeserializer()
+        )
+    );
+  }
+
+  @Test
+  public void testDeserializeNestedMap() throws JsonProcessingException
+  {
+    final Map<String, Object> map = ImmutableMap.of(
+        "rootKey",
+        "rootVal",
+        "innerMap",
+        ImmutableMap.of(
+            "innerKey",
+            "innerVal"
+        )
+    );
+    final String json = objectMapper.writeValueAsString(map);
+    final StringObjectPairList pairList = objectMapper.readValue(json, 
StringObjectPairList.class);
+    Assert.assertEquals(
+        new StringObjectPairList(
+            ImmutableList.of(
+                new NonnullPair<>("rootKey", "rootVal"),
+                new NonnullPair<>("innerMap", ImmutableMap.of("innerKey", 
"innerVal"))
+            )
+        ),
+        pairList
+    );
+  }
+}
diff --git 
a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java 
b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
index 42b707e..527a3d4 100644
--- a/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
+++ b/sql/src/main/java/org/apache/druid/sql/calcite/schema/SystemSchema.java
@@ -665,7 +665,7 @@ public class SystemSchema extends AbstractSchema
             druidNode.getHostAndPort(),
             druidNode.getHostAndTlsPort(),
             dataNodeService.getMaxSize(),
-            dataNodeService.getType(),
+            dataNodeService.getServerType(),
             dataNodeService.getTier(),
             dataNodeService.getPriority()
         );

---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to