Repository: eclipse/leshan
Pull request #879 (2020-08-27 09:43:01)

Add new Json decoder based on Jackson
Passing to jackson was driven by the need to **support Unsigned Long (64bits)** which is available through BigInteger support.

But there is other motivations : 
 - We also face some issue with minimal-json without any move from the maintainers => https://github.com/ralfstx/minimal-json/issues/88
- Jackson will be used for cbor maybe this could make the code more consistent. (and limit number of dependency)
- Jackson is considered as a realy fast json parser. 
  I need to do some tests to know if there are some benefits on this point (this could helps for https://github.com/eclipse/leshan/issues/729)

sbernard31: I did some benchmarks about "Jackson" vs "Minimal" for LwM2mJson Encoder/Decoder.

```
> java -jar target/benchmark.jar -bm avgt -tu us -wi 5 -i 10 -f 1 -t1 Lwm2mJsonBench                                                                                                  

# JMH 1.17.4 (released 1318 days ago, please consider updating!)
# VM version: JDK 11.0.8, VM 11.0.8+10-post-Debian-1deb10u1
# VM invoker: /usr/lib/jvm/java-11-openjdk-amd64/bin/java
# VM options: <none>
# Warmup: 5 iterations, 1 s each
# Measurement: 10 iterations, 1 s each
# Timeout: 10 min per iteration
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op

# Run complete. Total time: 00:03:06

Benchmark                          Mode  Cnt    Score   Error  Units
Lwm2mJsonBench.jacksonDecode       avgt   10    5,311 ± 0,055  us/op
Lwm2mJsonBench.minimalDecode       avgt   10    7,240 ± 0,173  us/op

Lwm2mJsonBench.jacksonDecodeBig    avgt   10  300,307 ± 3,237  us/op
Lwm2mJsonBench.minimalDecodeBig    avgt   10  408,399 ± 4,041  us/op

Lwm2mJsonBench.jacksonDecodeSmall  avgt   10    0,998 ± 0,036  us/op
Lwm2mJsonBench.minimalDecodeSmall  avgt   10    0,988 ± 0,007  us/op

Lwm2mJsonBench.jacksonEncode       avgt   10    5,317 ± 0,046  us/op
Lwm2mJsonBench.minimalEncode       avgt   10    5,095 ± 0,131  us/op

Lwm2mJsonBench.jacksonEncodeBig    avgt   10  287,959 ± 1,452  us/op
Lwm2mJsonBench.minimalEncodeBig    avgt   10  311,546 ± 1,672  us/op

Lwm2mJsonBench.jacksonEncodeSmall  avgt   10    0,761 ± 0,026  us/op
Lwm2mJsonBench.minimalEncodeSmall  avgt   10    0,680 ± 0,005  us/op
```

In this use case Jackson is either faster or as fast as minimal-json.
dachaac: If minmal-json is abandoned project -- why keep it in source tree?
sbernard31: > If minmal-json is abandoned project -- why keep it in source tree?

The idea is to remove dependency in the end, but I prefer to keep both encoder for now. (There is other source code which should be migrate, especially in demo we should not keep 3 dependency to Json libraries)
dachaac: Please check my minor comments but feel free to merge if you see it fit.

Patch:
diff --git a/leshan-core/pom.xml b/leshan-core/pom.xml
index c217ca82..57ecbc1c 100644
--- a/leshan-core/pom.xml
+++ b/leshan-core/pom.xml
@@ -38,6 +38,10 @@ Contributors:
             <groupId>com.eclipsesource.minimal-json</groupId>
             <artifactId>minimal-json</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
         <dependency>
             <groupId>com.fasterxml.jackson.dataformat</groupId>
             <artifactId>jackson-dataformat-cbor</artifactId>
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/json/LwM2mJsonDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/json/LwM2mJsonDecoder.java
new file mode 100644
index 00000000..621c9b7d
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/json/LwM2mJsonDecoder.java
@@ -0,0 +1,24 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.json;
+
+/**
+ * A decoder fo rthe old deprecated LwM2m format.
+ */
+public interface LwM2mJsonDecoder {
+
+    JsonRootObject fromJsonLwM2m(String jsonString) throws LwM2mJsonException;
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/json/LwM2mJsonEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/json/LwM2mJsonEncoder.java
new file mode 100644
index 00000000..e6319ed0
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/json/LwM2mJsonEncoder.java
@@ -0,0 +1,25 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.json;
+
+/**
+ * An encoder for the old deprecated LwM2m format.
+ */
+public interface LwM2mJsonEncoder {
+
+    public String toJsonLwM2m(JsonRootObject jro) throws LwM2mJsonException;
+
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/json/jackson/JsonArrayEntrySerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/core/json/jackson/JsonArrayEntrySerDes.java
new file mode 100644
index 00000000..360a27de
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/json/jackson/JsonArrayEntrySerDes.java
@@ -0,0 +1,117 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.json.jackson;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import org.eclipse.leshan.core.json.JsonArrayEntry;
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.util.json.JacksonJsonSerDes;
+import org.eclipse.leshan.core.util.json.JsonException;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+public class JsonArrayEntrySerDes extends JacksonJsonSerDes<JsonArrayEntry> {
+
+    @Override
+    public JsonNode jSerialize(JsonArrayEntry jae) throws JsonException {
+        ObjectNode o = JsonNodeFactory.instance.objectNode();
+        if (jae.getName() != null)
+            o.put("n", jae.getName());
+        Type type = jae.getType();
+        if (type != null) {
+            switch (jae.getType()) {
+            case FLOAT:
+                Number value = jae.getFloatValue();
+                // integer
+                if (value instanceof Byte) {
+                    o.put("v", value.byteValue());
+                } else if (value instanceof Short) {
+                    o.put("v", value.shortValue());
+                } else if (value instanceof Integer) {
+                    o.put("v", value.intValue());
+                } else if (value instanceof Long) {
+                    o.put("v", value.longValue());
+                } else if (value instanceof BigInteger) {
+                    o.put("v", (BigInteger) value);
+                }
+                // floating-point
+                else if (value instanceof Float) {
+                    o.put("v", value.floatValue());
+                } else if (value instanceof Double) {
+                    o.put("v", value.doubleValue());
+                } else if (value instanceof BigDecimal) {
+                    o.put("v", (BigDecimal) value);
+                }
+                break;
+            case BOOLEAN:
+                o.put("bv", jae.getBooleanValue());
+                break;
+            case OBJLNK:
+                o.put("ov", jae.getObjectLinkValue());
+                break;
+            case STRING:
+                o.put("sv", jae.getStringValue());
+                break;
+            default:
+                throw new JsonException("JsonArrayEntry MUST have a value : %s", jae);
+            }
+        }
+        if (jae.getTime() != null)
+            o.put("t", jae.getTime());
+        return o;
+    };
+
+    @Override
+    public JsonArrayEntry deserialize(JsonNode o) throws JsonException {
+        if (o == null)
+            return null;
+
+        JsonArrayEntry jae = new JsonArrayEntry();
+        JsonNode n = o.get("n");
+        if (n != null && n.isTextual())
+            jae.setName(n.asText());
+
+        JsonNode t = o.get("t");
+        if (t != null && t.isNumber())
+            jae.setTime(t.asLong());
+
+        JsonNode v = o.get("v");
+        if (v != null && v.isNumber())
+            jae.setFloatValue(v.numberValue());
+
+        JsonNode bv = o.get("bv");
+        if (bv != null && bv.isBoolean())
+            jae.setBooleanValue(bv.asBoolean());
+
+        JsonNode sv = o.get("sv");
+        if (sv != null && sv.isTextual())
+            jae.setStringValue(sv.asText());
+
+        JsonNode ov = o.get("ov");
+        if (ov != null && ov.isTextual())
+            jae.setObjectLinkValue(ov.asText());
+
+        if (jae.getType() == null) {
+            throw new JsonException("Missing value(v,bv,ov,sv) field for entry %s", o.toString());
+        }
+
+        return jae;
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/json/jackson/JsonRootObjectSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/core/json/jackson/JsonRootObjectSerDes.java
new file mode 100644
index 00000000..141afaf9
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/json/jackson/JsonRootObjectSerDes.java
@@ -0,0 +1,71 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.json.jackson;
+
+import org.eclipse.leshan.core.json.JsonRootObject;
+import org.eclipse.leshan.core.util.json.JacksonJsonSerDes;
+import org.eclipse.leshan.core.util.json.JsonException;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+public class JsonRootObjectSerDes extends JacksonJsonSerDes<JsonRootObject> {
+
+    private JsonArrayEntrySerDes serDes = new JsonArrayEntrySerDes();
+
+    @Override
+    public JsonNode jSerialize(JsonRootObject jro) throws JsonException {
+        ObjectNode o = JsonNodeFactory.instance.objectNode();
+
+        if (jro.getBaseName() != null)
+            o.put("bn", jro.getBaseName());
+
+        ArrayNode ja = serDes.jSerialize(jro.getResourceList());
+        if (ja != null)
+            o.set("e", ja);
+
+        if (jro.getBaseTime() != null)
+            o.put("bt", jro.getBaseTime());
+
+        return o;
+    };
+
+    @Override
+    public JsonRootObject deserialize(JsonNode jsonNode) throws JsonException {
+        if (jsonNode == null)
+            return null;
+
+        JsonRootObject jro = new JsonRootObject();
+
+        JsonNode e = jsonNode.get("e");
+        if (e != null && e.isArray())
+            jro.setResourceList(serDes.deserialize(e.elements()));
+        else
+            throw new JsonException("'e' field is missing for %s", jsonNode.toString());
+
+        JsonNode bn = jsonNode.get("bn");
+        if (bn != null && bn.isTextual())
+            jro.setBaseName(bn.asText());
+
+        JsonNode bt = jsonNode.get("bt");
+        if (bt != null && bt.isNumber())
+            jro.setBaseTime(bt.asLong());
+
+        return jro;
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/json/jackson/LwM2mJsonJacksonEncoderDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/json/jackson/LwM2mJsonJacksonEncoderDecoder.java
new file mode 100644
index 00000000..684b2b67
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/json/jackson/LwM2mJsonJacksonEncoderDecoder.java
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *     Gemalto M2M GmbH
+ *******************************************************************************/
+
+package org.eclipse.leshan.core.json.jackson;
+
+import java.io.IOException;
+
+import org.eclipse.leshan.core.json.JsonRootObject;
+import org.eclipse.leshan.core.json.LwM2mJsonDecoder;
+import org.eclipse.leshan.core.json.LwM2mJsonEncoder;
+import org.eclipse.leshan.core.json.LwM2mJsonException;
+import org.eclipse.leshan.core.util.json.JsonException;
+
+import com.eclipsesource.json.ParseException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Helper for encoding/decoding LWM2M JSON format using minimal-json
+ */
+public class LwM2mJsonJacksonEncoderDecoder implements LwM2mJsonDecoder, LwM2mJsonEncoder {
+
+    private static final JsonRootObjectSerDes serDes = new JsonRootObjectSerDes();
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    @Override
+    public String toJsonLwM2m(JsonRootObject jro) throws LwM2mJsonException {
+        try {
+            return serDes.sSerialize(jro);
+        } catch (JsonException e) {
+            throw new LwM2mJsonException("Unable to serialize LWM2M JSON.", e);
+        }
+    }
+
+    @Override
+    public JsonRootObject fromJsonLwM2m(String jsonString) throws LwM2mJsonException {
+        try {
+            return serDes.deserialize(mapper.readTree(jsonString));
+        } catch (JsonException | ParseException | IOException e) {
+            throw new LwM2mJsonException("Unable to parse LWM2M JSON.", e);
+        }
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/json/JsonArrayEntrySerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/core/json/minimaljson/JsonArrayEntrySerDes.java
similarity index 96%
rename from leshan-core/src/main/java/org/eclipse/leshan/core/json/JsonArrayEntrySerDes.java
rename to leshan-core/src/main/java/org/eclipse/leshan/core/json/minimaljson/JsonArrayEntrySerDes.java
index 69e20afb..7e79073b 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/json/JsonArrayEntrySerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/json/minimaljson/JsonArrayEntrySerDes.java
@@ -13,8 +13,9 @@
  * Contributors:
  *     Sierra Wireless - initial API and implementation
  *******************************************************************************/
-package org.eclipse.leshan.core.json;
+package org.eclipse.leshan.core.json.minimaljson;
 
+import org.eclipse.leshan.core.json.JsonArrayEntry;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.util.json.JsonException;
 import org.eclipse.leshan.core.util.json.JsonSerDes;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/json/JsonRootObjectSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/core/json/minimaljson/JsonRootObjectSerDes.java
similarity index 95%
rename from leshan-core/src/main/java/org/eclipse/leshan/core/json/JsonRootObjectSerDes.java
rename to leshan-core/src/main/java/org/eclipse/leshan/core/json/minimaljson/JsonRootObjectSerDes.java
index 72796cc4..2e7eb61f 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/json/JsonRootObjectSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/json/minimaljson/JsonRootObjectSerDes.java
@@ -13,8 +13,9 @@
  * Contributors:
  *     Sierra Wireless - initial API and implementation
  *******************************************************************************/
-package org.eclipse.leshan.core.json;
+package org.eclipse.leshan.core.json.minimaljson;
 
+import org.eclipse.leshan.core.json.JsonRootObject;
 import org.eclipse.leshan.core.util.json.JsonException;
 import org.eclipse.leshan.core.util.json.JsonSerDes;
 
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/json/LwM2mJson.java b/leshan-core/src/main/java/org/eclipse/leshan/core/json/minimaljson/LwM2mJsonMinimalEncoderDecoder.java
similarity index 69%
rename from leshan-core/src/main/java/org/eclipse/leshan/core/json/LwM2mJson.java
rename to leshan-core/src/main/java/org/eclipse/leshan/core/json/minimaljson/LwM2mJsonMinimalEncoderDecoder.java
index 8dd57d9e..48e1c952 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/json/LwM2mJson.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/json/minimaljson/LwM2mJsonMinimalEncoderDecoder.java
@@ -15,21 +15,26 @@
  *     Gemalto M2M GmbH
  *******************************************************************************/
 
-package org.eclipse.leshan.core.json;
+package org.eclipse.leshan.core.json.minimaljson;
 
+import org.eclipse.leshan.core.json.JsonRootObject;
+import org.eclipse.leshan.core.json.LwM2mJsonDecoder;
+import org.eclipse.leshan.core.json.LwM2mJsonEncoder;
+import org.eclipse.leshan.core.json.LwM2mJsonException;
 import org.eclipse.leshan.core.util.json.JsonException;
 
 import com.eclipsesource.json.Json;
 import com.eclipsesource.json.ParseException;
 
 /**
- * Helper for encoding/decoding LWM2M JSON format
+ * Helper for encoding/decoding LWM2M JSON format using minimal-json
  */
-public class LwM2mJson {
+public class LwM2mJsonMinimalEncoderDecoder implements LwM2mJsonDecoder, LwM2mJsonEncoder {
 
     private static final JsonRootObjectSerDes serDes = new JsonRootObjectSerDes();
 
-    public static String toJsonLwM2m(JsonRootObject jro) throws LwM2mJsonException {
+    @Override
+    public String toJsonLwM2m(JsonRootObject jro) throws LwM2mJsonException {
         try {
             return serDes.sSerialize(jro);
         } catch (JsonException e) {
@@ -37,7 +42,8 @@ public class LwM2mJson {
         }
     }
 
-    public static JsonRootObject fromJsonLwM2m(String jsonString) throws LwM2mJsonException {
+    @Override
+    public JsonRootObject fromJsonLwM2m(String jsonString) throws LwM2mJsonException {
         try {
             return serDes.deserialize(Json.parse(jsonString).asObject());
         } catch (JsonException | ParseException e) {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
index d8b5ae3a..bf2d681e 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
@@ -18,7 +18,9 @@ package org.eclipse.leshan.core.node.codec;
 
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.node.LwM2mNode;
@@ -50,7 +52,26 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
 
     private static final Logger LOG = LoggerFactory.getLogger(DefaultLwM2mNodeDecoder.class);
 
-    protected final boolean supportDeprecatedContentFormat;
+    public static Map<ContentFormat, NodeDecoder> getDefaultDecoders(boolean supportDeprecatedContentFormat) {
+        Map<ContentFormat, NodeDecoder> decoders = new HashMap<>();
+        decoders.put(ContentFormat.TEXT, new LwM2mNodeTextDecoder());
+        decoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueDecoder());
+
+        // tlv
+        LwM2mNodeTlvDecoder tlvDecoder = new LwM2mNodeTlvDecoder();
+        decoders.put(ContentFormat.TLV, tlvDecoder);
+        if (supportDeprecatedContentFormat)
+            decoders.put(new ContentFormat(ContentFormat.OLD_TLV_CODE), tlvDecoder);
+
+        // deprecated json
+        LwM2mNodeJsonDecoder jsonDecoder = new LwM2mNodeJsonDecoder();
+        decoders.put(ContentFormat.JSON, jsonDecoder);
+        if (supportDeprecatedContentFormat)
+            decoders.put(new ContentFormat(ContentFormat.OLD_JSON_CODE), jsonDecoder);
+        return decoders;
+    }
+
+    protected final Map<ContentFormat, NodeDecoder> decoders;
 
     /**
      * Create {@link DefaultLwM2mNodeDecoder} without support of old TLV and JSON code.
@@ -68,7 +89,11 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
      * @param supportDeprecatedContentFormat True to accept to decode old code.
      */
     public DefaultLwM2mNodeDecoder(boolean supportDeprecatedContentFormat) {
-        this.supportDeprecatedContentFormat = supportDeprecatedContentFormat;
+        this(getDefaultDecoders(supportDeprecatedContentFormat));
+    }
+
+    public DefaultLwM2mNodeDecoder(Map<ContentFormat, NodeDecoder> decoders) {
+        this.decoders = decoders;
     }
 
     @Override
@@ -78,7 +103,6 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
     }
 
     @Override
-    @SuppressWarnings("unchecked")
     public <T extends LwM2mNode> T decode(byte[] content, ContentFormat format, LwM2mPath path, LwM2mModel model,
             Class<T> nodeClass) throws CodecException {
 
@@ -89,25 +113,11 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
             throw new CodecException("Content format is mandatory. [%s]", path);
         }
 
-        if (!isSupported(format)) {
-            throw new CodecException("Content format %s is not supported [%s]", format, path);
-        }
-
-        // Decode content.
-        switch (format.getCode()) {
-        case ContentFormat.TEXT_CODE:
-            return (T) LwM2mNodeTextDecoder.decode(content, path, model);
-        case ContentFormat.TLV_CODE:
-        case ContentFormat.OLD_TLV_CODE:
-            return LwM2mNodeTlvDecoder.decode(content, path, model, nodeClass);
-        case ContentFormat.OPAQUE_CODE:
-            return (T) LwM2mNodeOpaqueDecoder.decode(content, path, model);
-        case ContentFormat.JSON_CODE:
-        case ContentFormat.OLD_JSON_CODE:
-            return LwM2mNodeJsonDecoder.decode(content, path, model, nodeClass);
-        default:
+        NodeDecoder decoder = decoders.get(format);
+        if (decoder == null) {
             throw new CodecException("Content format %s is not supported [%s]", format, path);
         }
+        return decoder.decode(content, path, model, nodeClass);
     }
 
     @Override
@@ -120,24 +130,16 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
             throw new CodecException("Content format is mandatory. [%s]", path);
         }
 
-        if (!isSupported(format)) {
+        NodeDecoder decoder = decoders.get(format);
+        if (decoder == null) {
             throw new CodecException("Content format %s is not supported [%s]", format, path);
         }
 
-        // Decode content.
-        switch (format.getCode()) {
-        case ContentFormat.TEXT_CODE:
-            return toTimestampedNodes(LwM2mNodeTextDecoder.decode(content, path, model));
-        case ContentFormat.TLV_CODE:
-        case ContentFormat.OLD_TLV_CODE:
-            return toTimestampedNodes(LwM2mNodeTlvDecoder.decode(content, path, model, nodeClassFromPath(path)));
-        case ContentFormat.OPAQUE_CODE:
-            return toTimestampedNodes(LwM2mNodeOpaqueDecoder.decode(content, path, model));
-        case ContentFormat.JSON_CODE:
-        case ContentFormat.OLD_JSON_CODE:
-            return LwM2mNodeJsonDecoder.decodeTimestamped(content, path, model, nodeClassFromPath(path));
-        default:
-            throw new CodecException("Content format %s is not supported [%s]", format, path);
+        if (decoder instanceof TimestampedNodeDecoder) {
+            return ((TimestampedNodeDecoder) decoder).decodeTimestampedData(content, path, model,
+                    nodeClassFromPath(path));
+        } else {
+            return toTimestampedNodes(decoder.decode(content, path, model, nodeClassFromPath(path)));
         }
     }
 
@@ -165,17 +167,6 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
 
     @Override
     public boolean isSupported(ContentFormat format) {
-        switch (format.getCode()) {
-        case ContentFormat.TEXT_CODE:
-        case ContentFormat.TLV_CODE:
-        case ContentFormat.OPAQUE_CODE:
-        case ContentFormat.JSON_CODE:
-            return true;
-        case ContentFormat.OLD_TLV_CODE:
-        case ContentFormat.OLD_JSON_CODE:
-            return supportDeprecatedContentFormat;
-        default:
-            return false;
-        }
+        return decoders.get(format) != null;
     }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
index 4ba99d1e..f64b6dfb 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
@@ -16,7 +16,9 @@
  *******************************************************************************/
 package org.eclipse.leshan.core.node.codec;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.node.LwM2mNode;
@@ -45,8 +47,28 @@ public class DefaultLwM2mNodeEncoder implements LwM2mNodeEncoder {
 
     private static final Logger LOG = LoggerFactory.getLogger(DefaultLwM2mNodeEncoder.class);
 
+    public static Map<ContentFormat, NodeEncoder> getDefaultEncoders(boolean supportDeprecatedContentFormat) {
+        Map<ContentFormat, NodeEncoder> encoders = new HashMap<>();
+        encoders.put(ContentFormat.TEXT, new LwM2mNodeTextEncoder());
+        encoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueEncoder());
+        encoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLJsonEncoder());
+
+        // tlv
+        LwM2mNodeTlvEncoder tlvDecoder = new LwM2mNodeTlvEncoder();
+        encoders.put(ContentFormat.TLV, tlvDecoder);
+        if (supportDeprecatedContentFormat)
+            encoders.put(new ContentFormat(ContentFormat.OLD_TLV_CODE), tlvDecoder);
+
+        // deprecated json
+        LwM2mNodeJsonEncoder jsonDecoder = new LwM2mNodeJsonEncoder();
+        encoders.put(ContentFormat.JSON, jsonDecoder);
+        if (supportDeprecatedContentFormat)
+            encoders.put(new ContentFormat(ContentFormat.OLD_JSON_CODE), jsonDecoder);
+        return encoders;
+    }
+
     protected final LwM2mValueConverter converter;
-    protected final boolean supportDeprecatedContentFormat;
+    protected final Map<ContentFormat, NodeEncoder> encoders;
 
     /**
      * Create {@link DefaultLwM2mNodeEncoder} without support of old TLV and JSON code.
@@ -72,8 +94,12 @@ public class DefaultLwM2mNodeEncoder implements LwM2mNodeEncoder {
     }
 
     public DefaultLwM2mNodeEncoder(LwM2mValueConverter converter, boolean supportDeprecatedContentFormat) {
+        this(getDefaultEncoders(supportDeprecatedContentFormat), converter);
+    }
+
+    public DefaultLwM2mNodeEncoder(Map<ContentFormat, NodeEncoder> encoders, LwM2mValueConverter converter) {
+        this.encoders = encoders;
         this.converter = converter;
-        this.supportDeprecatedContentFormat = supportDeprecatedContentFormat;
     }
 
     @Override
@@ -84,33 +110,12 @@ public class DefaultLwM2mNodeEncoder implements LwM2mNodeEncoder {
             throw new CodecException("Content format is mandatory. [%s]", path);
         }
 
-        if (!isSupported(format)) {
+        NodeEncoder encoder = encoders.get(format);
+        if (encoder == null) {
             throw new CodecException("Content format %s is not supported [%s]", format, path);
         }
-
         LOG.trace("Encoding node {} for path {} and format {}", node, path, format);
-        byte[] encoded;
-        switch (format.getCode()) {
-        case ContentFormat.TLV_CODE:
-        case ContentFormat.OLD_TLV_CODE:
-            encoded = LwM2mNodeTlvEncoder.encode(node, path, model, converter);
-            break;
-        case ContentFormat.TEXT_CODE:
-            encoded = LwM2mNodeTextEncoder.encode(node, path, model, converter);
-            break;
-        case ContentFormat.OPAQUE_CODE:
-            encoded = LwM2mNodeOpaqueEncoder.encode(node, path, model, converter);
-            break;
-        case ContentFormat.JSON_CODE:
-        case ContentFormat.OLD_JSON_CODE:
-            encoded = LwM2mNodeJsonEncoder.encode(node, path, model, converter);
-            break;
-        case ContentFormat.SENML_JSON_CODE:
-            encoded = LwM2mNodeSenMLJsonEncoder.encode(node, path, model, converter);
-            break;
-        default:
-            throw new CodecException("Content format %s is not supported [%s]", format, path);
-        }
+        byte[] encoded = encoder.encode(node, path, model, converter);
         LOG.trace("Encoded node {}: {}", node, encoded);
         return encoded;
     }
@@ -123,39 +128,23 @@ public class DefaultLwM2mNodeEncoder implements LwM2mNodeEncoder {
             throw new CodecException("Content format is mandatory. [%s]", path);
         }
 
-        if (!isSupported(format)) {
+        NodeEncoder encoder = encoders.get(format);
+        if (encoder == null) {
             throw new CodecException("Content format %s is not supported [%s]", format, path);
         }
-
-        LOG.trace("Encoding time-stamped nodes for path {} and format {}", timestampedNodes, path, format);
-        byte[] encoded;
-        switch (format.getCode()) {
-        case ContentFormat.JSON_CODE:
-        case ContentFormat.OLD_JSON_CODE:
-            encoded = LwM2mNodeJsonEncoder.encodeTimestampedData(timestampedNodes, path, model, converter);
-            break;
-        default:
+        if (!(encoder instanceof TimestampedNodeEncoder)) {
             throw new CodecException("Cannot encode timestampedNode with format %s. [%s]", format, path);
         }
-
+        LOG.trace("Encoding time-stamped nodes for path {} and format {}", timestampedNodes, path, format);
+        byte[] encoded = ((TimestampedNodeEncoder) encoder).encodeTimestampedData(timestampedNodes, path, model,
+                converter);
         LOG.trace("Encoded node timestampedNode: {}", timestampedNodes, encoded);
         return encoded;
+
     }
 
     @Override
     public boolean isSupported(ContentFormat format) {
-        switch (format.getCode()) {
-        case ContentFormat.TEXT_CODE:
-        case ContentFormat.TLV_CODE:
-        case ContentFormat.OPAQUE_CODE:
-        case ContentFormat.JSON_CODE:
-        case ContentFormat.SENML_JSON_CODE:
-            return true;
-        case ContentFormat.OLD_TLV_CODE:
-        case ContentFormat.OLD_JSON_CODE:
-            return supportDeprecatedContentFormat;
-        default:
-            return false;
-        }
+        return encoders.get(format) != null;
     }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoder.java
index 2786633a..085b8d18 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoder.java
@@ -23,6 +23,9 @@ import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.request.ContentFormat;
 
+/**
+ * A Decoder of {@link LwM2mNode} which should support several {@link ContentFormat} and time-stamped values.
+ */
 public interface LwM2mNodeDecoder {
 
     /**
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoder.java
index c4997684..8aff71d7 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoder.java
@@ -23,6 +23,9 @@ import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.request.ContentFormat;
 
+/**
+ * An Encoder of {@link LwM2mNode} which should support several {@link ContentFormat} and time-stamped values.
+ */
 public interface LwM2mNodeEncoder {
 
     /**
@@ -35,8 +38,7 @@ public interface LwM2mNodeEncoder {
      * @return the encoded node as a byte array
      * @throws CodecException if there payload is malformed.
      */
-    byte[] encode(LwM2mNode node, ContentFormat format, LwM2mPath path, LwM2mModel model)
-            throws CodecException;
+    byte[] encode(LwM2mNode node, ContentFormat format, LwM2mPath path, LwM2mModel model) throws CodecException;
 
     /**
      * Serializes a list of time-stamped {@link LwM2mNode} with the given content format.
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/NodeDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/NodeDecoder.java
new file mode 100644
index 00000000..a3fa545b
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/NodeDecoder.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.node.codec;
+
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.request.ContentFormat;
+
+/**
+ * A decoder for {@link LwM2mNode} which support only one {@link ContentFormat}.
+ * 
+ * @see DefaultLwM2mNodeDecoder
+ */
+public interface NodeDecoder {
+
+    /**
+     * Deserializes a binary content into a {@link LwM2mNode} of the expected type.
+     *
+     * @param content the content
+     * @param path the path of the node to build
+     * @param model the collection of supported object models
+     * @param nodeClass the class of the {@link LwM2mNode} to decode
+     * @return the resulting node
+     * 
+     * @throws CodecException if there payload is malformed.
+     */
+    <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
+            throws CodecException;
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/NodeEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/NodeEncoder.java
new file mode 100644
index 00000000..bc690aca
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/NodeEncoder.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.node.codec;
+
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.request.ContentFormat;
+
+/**
+ * An encoder for {@link LwM2mNode} which support only one {@link ContentFormat}.
+ * 
+ * @see DefaultLwM2mNodeEncoder
+ */
+public interface NodeEncoder {
+
+    /**
+     * Serializes a {@link LwM2mNode}.
+     *
+     * @param node the object/instance/resource to serialize
+     * @param path the path of the node to serialize
+     * @param model the collection of supported object models
+     * @param converter a data type converter.
+     * @return the encoded node as a byte array
+     * 
+     * @throws CodecException if there payload is malformed.
+     */
+    byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter)
+            throws CodecException;
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedNodeDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedNodeDecoder.java
new file mode 100644
index 00000000..fa36ef14
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedNodeDecoder.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.node.codec;
+
+import java.util.List;
+
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
+
+/**
+ * A {@link NodeDecoder} which can also decode time-stamped values.
+ * 
+ * @see DefaultLwM2mNodeDecoder
+ */
+public interface TimestampedNodeDecoder extends NodeDecoder {
+    /**
+     * Deserializes a binary content into a list of time-stamped {@link LwM2mNode} ordering by time-stamp.
+     *
+     * @param content the content
+     * @param path the path of the node to build
+     * @param model the collection of supported object models
+     * @param nodeClass the class of the {@link LwM2mNode} to decode
+     * @return the resulting list of time-stamped {@link LwM2mNode} ordering by time-stamp
+     * @exception CodecException if there payload is malformed.
+     */
+    List<TimestampedLwM2mNode> decodeTimestampedData(byte[] content, LwM2mPath path, LwM2mModel model,
+            Class<? extends LwM2mNode> nodeClass) throws CodecException;
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedNodeEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedNodeEncoder.java
new file mode 100644
index 00000000..f175de79
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/TimestampedNodeEncoder.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.node.codec;
+
+import java.util.List;
+
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
+
+/**
+ * A {@link NodeEncoder} which can also encode time-stamped values.
+ * 
+ * @see DefaultLwM2mNodeEncoder
+ */
+public interface TimestampedNodeEncoder extends NodeEncoder {
+
+    /**
+     * Serializes a list of time-stamped {@link LwM2mNode}.
+     *
+     * @param timestampedNodes the list of time-stamped object/instance/resource to serialize
+     * @param path the path of the node to serialize
+     * @param model the collection of supported object models
+     * @param converter a data type converter.
+     * @return the encoded node as a byte array
+     * @throws CodecException if there payload is malformed.
+     */
+    byte[] encodeTimestampedData(List<TimestampedLwM2mNode> timestampedNodes, LwM2mPath path, LwM2mModel model,
+            LwM2mValueConverter converter) throws CodecException;
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java
index 6f094a35..73257785 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java
@@ -29,8 +29,9 @@ import java.util.TreeMap;
 
 import org.eclipse.leshan.core.json.JsonArrayEntry;
 import org.eclipse.leshan.core.json.JsonRootObject;
-import org.eclipse.leshan.core.json.LwM2mJson;
+import org.eclipse.leshan.core.json.LwM2mJsonDecoder;
 import org.eclipse.leshan.core.json.LwM2mJsonException;
+import org.eclipse.leshan.core.json.jackson.LwM2mJsonJacksonEncoderDecoder;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
@@ -45,20 +46,32 @@ import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.core.node.codec.TimestampedNodeDecoder;
 import org.eclipse.leshan.core.util.Base64;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeJsonDecoder {
+public class LwM2mNodeJsonDecoder implements TimestampedNodeDecoder {
 
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeJsonDecoder.class);
 
+    private final LwM2mJsonDecoder decoder;
+
+    public LwM2mNodeJsonDecoder() {
+        this.decoder = new LwM2mJsonJacksonEncoderDecoder();
+    }
+
+    public LwM2mNodeJsonDecoder(LwM2mJsonDecoder jsonDecoder) {
+        this.decoder = jsonDecoder;
+    }
+
     @SuppressWarnings("unchecked")
-    public static <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
+    @Override
+    public <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
             throws CodecException {
         try {
             String jsonStrValue = content != null ? new String(content) : "";
-            JsonRootObject json = LwM2mJson.fromJsonLwM2m(jsonStrValue);
+            JsonRootObject json = decoder.fromJsonLwM2m(jsonStrValue);
             List<TimestampedLwM2mNode> timestampedNodes = parseJSON(json, path, model, nodeClass);
             if (timestampedNodes.size() == 0) {
                 return null;
@@ -71,19 +84,20 @@ public class LwM2mNodeJsonDecoder {
         }
     }
 
-    public static List<TimestampedLwM2mNode> decodeTimestamped(byte[] content, LwM2mPath path, LwM2mModel model,
+    @Override
+    public List<TimestampedLwM2mNode> decodeTimestampedData(byte[] content, LwM2mPath path, LwM2mModel model,
             Class<? extends LwM2mNode> nodeClass) throws CodecException {
         try {
             String jsonStrValue = new String(content);
-            JsonRootObject json = LwM2mJson.fromJsonLwM2m(jsonStrValue);
+            JsonRootObject json = decoder.fromJsonLwM2m(jsonStrValue);
             return parseJSON(json, path, model, nodeClass);
         } catch (LwM2mJsonException e) {
             throw new CodecException(e, "Unable to deserialize json [path:%s]", path);
         }
     }
 
-    private static List<TimestampedLwM2mNode> parseJSON(JsonRootObject jsonObject, LwM2mPath requestPath,
-            LwM2mModel model, Class<? extends LwM2mNode> nodeClass) throws CodecException {
+    private List<TimestampedLwM2mNode> parseJSON(JsonRootObject jsonObject, LwM2mPath requestPath, LwM2mModel model,
+            Class<? extends LwM2mNode> nodeClass) throws CodecException {
 
         LOG.trace("Parsing JSON content for path {}: {}", requestPath, jsonObject);
 
@@ -182,7 +196,7 @@ public class LwM2mNodeJsonDecoder {
 
     }
 
-    private static Long computeTimestamp(Long baseTime, Long time) {
+    private Long computeTimestamp(Long baseTime, Long time) {
         Long timestamp;
         if (baseTime != null) {
             if (time != null) {
@@ -205,7 +219,7 @@ public class LwM2mNodeJsonDecoder {
      * 
      * @return a map (relativeTimestamp => collection of JsonArrayEntry)
      */
-    private static SortedMap<Long, Collection<JsonArrayEntry>> groupJsonEntryByTimestamp(JsonRootObject jsonObject) {
+    private SortedMap<Long, Collection<JsonArrayEntry>> groupJsonEntryByTimestamp(JsonRootObject jsonObject) {
         SortedMap<Long, Collection<JsonArrayEntry>> result = new TreeMap<>(new Comparator<Long>() {
             @Override
             public int compare(Long o1, Long o2) {
@@ -247,8 +261,8 @@ public class LwM2mNodeJsonDecoder {
      *
      * @return a map (instanceId => collection of JsonArrayEntry)
      */
-    private static Map<Integer, Collection<JsonArrayEntry>> groupJsonEntryByInstanceId(
-            Collection<JsonArrayEntry> jsonEntries, String baseName, LwM2mPath requestPath) throws CodecException {
+    private Map<Integer, Collection<JsonArrayEntry>> groupJsonEntryByInstanceId(Collection<JsonArrayEntry> jsonEntries,
+            String baseName, LwM2mPath requestPath) throws CodecException {
         Map<Integer, Collection<JsonArrayEntry>> result = new HashMap<>();
 
         for (JsonArrayEntry e : jsonEntries) {
@@ -290,7 +304,7 @@ public class LwM2mNodeJsonDecoder {
         return result;
     }
 
-    private static LwM2mPath extractAndValidatePath(String baseName, String name, LwM2mPath requestPath)
+    private LwM2mPath extractAndValidatePath(String baseName, String name, LwM2mPath requestPath)
             throws CodecException {
         LwM2mPath path = new LwM2mPath(baseName + name);
 
@@ -315,7 +329,7 @@ public class LwM2mNodeJsonDecoder {
         return path;
     }
 
-    private static Map<Integer, LwM2mResource> extractLwM2mResources(Collection<JsonArrayEntry> jsonArrayEntries,
+    private Map<Integer, LwM2mResource> extractLwM2mResources(Collection<JsonArrayEntry> jsonArrayEntries,
             String baseName, LwM2mModel model, LwM2mPath requestPath) throws CodecException {
         if (jsonArrayEntries == null)
             return Collections.emptyMap();
@@ -413,7 +427,7 @@ public class LwM2mNodeJsonDecoder {
         return lwM2mResourceMap;
     }
 
-    private static Object parseJsonValue(Object value, Type expectedType, LwM2mPath path) throws CodecException {
+    private Object parseJsonValue(Object value, Type expectedType, LwM2mPath path) throws CodecException {
 
         LOG.trace("JSON value for path {} and expected type {}: {}", path, expectedType, value);
 
@@ -443,7 +457,7 @@ public class LwM2mNodeJsonDecoder {
         }
     }
 
-    public static Type getResourceType(LwM2mPath rscPath, LwM2mModel model, JsonArrayEntry resourceElt) {
+    public Type getResourceType(LwM2mPath rscPath, LwM2mModel model, JsonArrayEntry resourceElt) {
         // Use model type in priority
         ResourceModel rscDesc = model.getResourceModel(rscPath.getObjectId(), rscPath.getResourceId());
         if (rscDesc != null)
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonEncoder.java
index 9575cac4..b74705f6 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonEncoder.java
@@ -22,8 +22,9 @@ import java.util.Map.Entry;
 
 import org.eclipse.leshan.core.json.JsonArrayEntry;
 import org.eclipse.leshan.core.json.JsonRootObject;
-import org.eclipse.leshan.core.json.LwM2mJson;
+import org.eclipse.leshan.core.json.LwM2mJsonEncoder;
 import org.eclipse.leshan.core.json.LwM2mJsonException;
+import org.eclipse.leshan.core.json.jackson.LwM2mJsonJacksonEncoderDecoder;
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
@@ -37,16 +38,28 @@ import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
+import org.eclipse.leshan.core.node.codec.TimestampedNodeEncoder;
 import org.eclipse.leshan.core.util.Base64;
 import org.eclipse.leshan.core.util.Validate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeJsonEncoder {
+public class LwM2mNodeJsonEncoder implements TimestampedNodeEncoder {
 
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeJsonEncoder.class);
 
-    public static byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter)
+    private final LwM2mJsonEncoder encoder;
+
+    public LwM2mNodeJsonEncoder() {
+        this.encoder = new LwM2mJsonJacksonEncoderDecoder();
+    }
+
+    public LwM2mNodeJsonEncoder(LwM2mJsonEncoder jsonEncoder) {
+        this.encoder = jsonEncoder;
+    }
+
+    @Override
+    public byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter)
             throws CodecException {
         Validate.notNull(node);
         Validate.notNull(path);
@@ -62,14 +75,15 @@ public class LwM2mNodeJsonEncoder {
         jsonObject.setResourceList(internalEncoder.resourceList);
         jsonObject.setBaseName(internalEncoder.baseName);
         try {
-            return LwM2mJson.toJsonLwM2m(jsonObject).getBytes();
+            return encoder.toJsonLwM2m(jsonObject).getBytes();
         } catch (LwM2mJsonException e) {
             throw new CodecException(e, "Unable to encode node[path:%s] : %s", path, node);
         }
     }
 
-    public static byte[] encodeTimestampedData(List<TimestampedLwM2mNode> timestampedNodes, LwM2mPath path,
-            LwM2mModel model, LwM2mValueConverter converter) {
+    @Override
+    public byte[] encodeTimestampedData(List<TimestampedLwM2mNode> timestampedNodes, LwM2mPath path, LwM2mModel model,
+            LwM2mValueConverter converter) {
         Validate.notNull(timestampedNodes);
         Validate.notNull(path);
         Validate.notNull(model);
@@ -100,7 +114,7 @@ public class LwM2mNodeJsonEncoder {
         jsonObject.setResourceList(entries);
         jsonObject.setBaseName(internalEncoder.baseName);
         try {
-            return LwM2mJson.toJsonLwM2m(jsonObject).getBytes();
+            return encoder.toJsonLwM2m(jsonObject).getBytes();
         } catch (LwM2mJsonException e) {
             throw new CodecException(e, "Unable to encode timestamped nodes[path:%s] : %s", path, timestampedNodes);
         }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueDecoder.java
index bcd37498..b2c1b91c 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueDecoder.java
@@ -22,10 +22,14 @@ import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.core.node.codec.NodeDecoder;
 
-public class LwM2mNodeOpaqueDecoder {
+public class LwM2mNodeOpaqueDecoder implements NodeDecoder {
 
-    public static LwM2mNode decode(byte[] content, LwM2mPath path, LwM2mModel model) throws CodecException {
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
+            throws CodecException {
         if (!path.isResource())
             throw new CodecException("Invalid path %s : OpaqueDecoder decodes resource only", path);
 
@@ -35,7 +39,6 @@ public class LwM2mNodeOpaqueDecoder {
                     "Invalid content format [%s] for path [%s], OPAQUE can only be used for single OPAQUE resource",
                     desc.type, path);
         }
-        return LwM2mSingleResource.newBinaryResource(path.getResourceId(), content != null ? content : new byte[0]);
+        return (T) LwM2mSingleResource.newBinaryResource(path.getResourceId(), content != null ? content : new byte[0]);
     }
-
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java
index 560ac2b6..21197678 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java
@@ -27,14 +27,16 @@ import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
+import org.eclipse.leshan.core.node.codec.NodeEncoder;
 import org.eclipse.leshan.core.util.Validate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeOpaqueEncoder {
+public class LwM2mNodeOpaqueEncoder implements NodeEncoder {
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeOpaqueEncoder.class);
 
-    public static byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter)
+    @Override
+    public byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter)
             throws CodecException {
         Validate.notNull(node);
         Validate.notNull(path);
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
index e3a9b0b2..5916b66a 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
@@ -29,6 +29,7 @@ import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
+import org.eclipse.leshan.core.node.codec.NodeEncoder;
 import org.eclipse.leshan.core.util.Base64;
 import org.eclipse.leshan.core.util.Validate;
 import org.eclipse.leshan.senml.SenMLJson;
@@ -37,10 +38,11 @@ import org.eclipse.leshan.senml.SenMLRecord;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeSenMLJsonEncoder {
+public class LwM2mNodeSenMLJsonEncoder implements NodeEncoder {
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeSenMLJsonEncoder.class);
 
-    public static byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter) {
+    @Override
+    public byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter) {
         Validate.notNull(node);
         Validate.notNull(path);
         Validate.notNull(model);
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java
index 4b8af53f..8eb24bd3 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java
@@ -28,14 +28,18 @@ import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.core.node.codec.NodeDecoder;
 import org.eclipse.leshan.core.util.Base64;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeTextDecoder {
+public class LwM2mNodeTextDecoder implements NodeDecoder {
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeTextDecoder.class);
 
-    public static LwM2mNode decode(byte[] content, LwM2mPath path, LwM2mModel model) throws CodecException {
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
+            throws CodecException {
         if (!path.isResource() && !path.isResourceInstance())
             throw new CodecException("Invalid path %s : TextDecoder decodes resource OR resource instance only", path);
 
@@ -45,22 +49,23 @@ public class LwM2mNodeTextDecoder {
 
         if (path.isResource()) {
             if (rDesc != null) {
-                return LwM2mSingleResource.newResource(path.getResourceId(), parseTextValue(strValue, rDesc.type, path),
-                        rDesc.type);
+                return (T) LwM2mSingleResource.newResource(path.getResourceId(),
+                        parseTextValue(strValue, rDesc.type, path), rDesc.type);
             }
+
             // unknown resource, returning a default string value
-            return LwM2mSingleResource.newStringResource(path.getResourceId(), strValue);
+            return (T) LwM2mSingleResource.newStringResource(path.getResourceId(), strValue);
         }
 
         if (rDesc != null) {
-            return LwM2mResourceInstance.newInstance(path.getResourceInstanceId(),
+            return (T) LwM2mResourceInstance.newInstance(path.getResourceInstanceId(),
                     parseTextValue(strValue, rDesc.type, path), rDesc.type);
         }
         // unknown resource, returning a default string value
-        return LwM2mResourceInstance.newStringInstance(path.getResourceInstanceId(), strValue);
+        return (T) LwM2mResourceInstance.newStringInstance(path.getResourceInstanceId(), strValue);
     }
 
-    private static Object parseTextValue(String value, Type type, LwM2mPath path) throws CodecException {
+    private Object parseTextValue(String value, Type type, LwM2mPath path) throws CodecException {
         LOG.trace("TEXT value for path {} and expected type {}: {}", path, type, value);
 
         switch (type) {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java
index f8ba3de1..adb94747 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java
@@ -31,16 +31,18 @@ import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
+import org.eclipse.leshan.core.node.codec.NodeEncoder;
 import org.eclipse.leshan.core.util.Base64;
 import org.eclipse.leshan.core.util.Validate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeTextEncoder {
+public class LwM2mNodeTextEncoder implements NodeEncoder {
 
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeTextEncoder.class);
 
-    public static byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter)
+    @Override
+    public byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter)
             throws CodecException {
         Validate.notNull(node);
         Validate.notNull(path);
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java
index 74f22a35..6b9de795 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java
@@ -34,6 +34,7 @@ import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.core.node.codec.NodeDecoder;
 import org.eclipse.leshan.core.tlv.Tlv;
 import org.eclipse.leshan.core.tlv.Tlv.TlvType;
 import org.eclipse.leshan.core.tlv.TlvDecoder;
@@ -42,11 +43,12 @@ import org.eclipse.leshan.core.util.Hex;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeTlvDecoder {
+public class LwM2mNodeTlvDecoder implements NodeDecoder {
 
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeTlvDecoder.class);
 
-    public static <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
+    @Override
+    public <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
             throws CodecException {
         try {
             Tlv[] tlvs = TlvDecoder.decode(ByteBuffer.wrap(content != null ? content : new byte[0]));
@@ -57,7 +59,7 @@ public class LwM2mNodeTlvDecoder {
     }
 
     @SuppressWarnings("unchecked")
-    private static <T extends LwM2mNode> T parseTlv(Tlv[] tlvs, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
+    private <T extends LwM2mNode> T parseTlv(Tlv[] tlvs, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
             throws CodecException {
         LOG.trace("Parsing TLV content for path {}: {}", path, tlvs);
 
@@ -183,8 +185,8 @@ public class LwM2mNodeTlvDecoder {
         throw new IllegalArgumentException("invalid node class: " + nodeClass);
     }
 
-    private static LwM2mObjectInstance parseObjectInstanceTlv(Tlv[] rscTlvs, int objectId, int instanceId,
-            LwM2mModel model) throws CodecException {
+    private LwM2mObjectInstance parseObjectInstanceTlv(Tlv[] rscTlvs, int objectId, int instanceId, LwM2mModel model)
+            throws CodecException {
         Map<Integer, LwM2mResource> resources = new HashMap<>(rscTlvs.length);
         for (Tlv rscTlv : rscTlvs) {
             LwM2mPath resourcePath = new LwM2mPath(objectId, instanceId, rscTlv.getIdentifier());
@@ -199,7 +201,7 @@ public class LwM2mNodeTlvDecoder {
 
     }
 
-    private static LwM2mObjectInstance parseObjectInstanceTlvWithoutId(Tlv[] rscTlvs, int objectId, LwM2mModel model)
+    private LwM2mObjectInstance parseObjectInstanceTlvWithoutId(Tlv[] rscTlvs, int objectId, LwM2mModel model)
             throws CodecException {
         Map<Integer, LwM2mResource> resources = new HashMap<>(rscTlvs.length);
         for (Tlv rscTlv : rscTlvs) {
@@ -214,8 +216,7 @@ public class LwM2mNodeTlvDecoder {
         return new LwM2mObjectInstance(resources.values());
     }
 
-    private static LwM2mResource parseResourceTlv(Tlv tlv, LwM2mPath resourcePath, LwM2mModel model)
-            throws CodecException {
+    private LwM2mResource parseResourceTlv(Tlv tlv, LwM2mPath resourcePath, LwM2mModel model) throws CodecException {
         Type expectedType = getResourceType(resourcePath, model);
         Integer resourceId = tlv.getIdentifier();
         switch (tlv.getType()) {
@@ -229,23 +230,27 @@ public class LwM2mNodeTlvDecoder {
         }
     }
 
-    private static LwM2mMultipleResource parseResourceInstancesTlv(Tlv[] tlvs, LwM2mPath resourcePath,
-            Type expectedType) throws CodecException {
+    private LwM2mMultipleResource parseResourceInstancesTlv(Tlv[] tlvs, LwM2mPath resourcePath, Type expectedType)
+            throws CodecException {
         Map<Integer, LwM2mResourceInstance> instances = new HashMap<>(tlvs.length);
-        for (Tlv tlv : tlvs) {
-            LwM2mResourceInstance resourceInstance = parseResourceInstanceTlv(tlv,
-                    resourcePath.append(tlv.getIdentifier()), expectedType);
-            LwM2mResourceInstance previousResourceInstance = instances.put(tlv.getIdentifier(), resourceInstance);
+        for (Tlv tlvChild : tlvs) {
+            if (tlvChild.getType() != TlvType.RESOURCE_INSTANCE)
+                throw new CodecException("Expected TLV of type RESOURCE_INSTANCE but was %s for path %s",
+                        tlvChild.getType().name(), resourcePath.append(tlvChild.getIdentifier()));
+
+            LwM2mResourceInstance resourceInstance = parseResourceInstanceTlv(tlvChild,
+                    resourcePath.append(tlvChild.getIdentifier()), expectedType);
+            LwM2mResourceInstance previousResourceInstance = instances.put(tlvChild.getIdentifier(), resourceInstance);
             if (previousResourceInstance != null) {
                 throw new CodecException("2 RESOURCE_INSTANCE nodes (%s,%s) with the same identifier %d for path %s",
-                        previousResourceInstance, resourceInstance, tlv.getIdentifier(), resourcePath);
+                        previousResourceInstance, resourceInstance, tlvChild.getIdentifier(), resourcePath);
             }
         }
         return new LwM2mMultipleResource(resourcePath.getResourceId(), expectedType, instances.values());
     }
 
-    private static LwM2mResourceInstance parseResourceInstanceTlv(Tlv tlv, LwM2mPath resourceInstancePath,
-            Type expectedType) throws CodecException {
+    private LwM2mResourceInstance parseResourceInstanceTlv(Tlv tlv, LwM2mPath resourceInstancePath, Type expectedType)
+            throws CodecException {
         Integer resourceInstanceId = tlv.getIdentifier();
         if (tlv.getType() != TlvType.RESOURCE_INSTANCE) {
             throw new CodecException("Expected TLV of type RESOURCE_INSTANCE but was %s for path %s", tlv.getType(),
@@ -257,7 +262,7 @@ public class LwM2mNodeTlvDecoder {
 
     }
 
-    private static Object parseTlvValue(byte[] value, Type expectedType, LwM2mPath path) throws CodecException {
+    private Object parseTlvValue(byte[] value, Type expectedType, LwM2mPath path) throws CodecException {
         try {
             LOG.trace("TLV value for path {} and expected type {}: {}", path, expectedType, value);
             switch (expectedType) {
@@ -284,7 +289,7 @@ public class LwM2mNodeTlvDecoder {
         }
     }
 
-    public static Type getResourceType(LwM2mPath rscPath, LwM2mModel model) throws CodecException {
+    public Type getResourceType(LwM2mPath rscPath, LwM2mModel model) throws CodecException {
         ResourceModel rscDesc = model.getResourceModel(rscPath.getObjectId(), rscPath.getResourceId());
         if (rscDesc == null) {
             LOG.trace("unknown type for resource : {}", rscPath);
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java
index fa9f0f7b..2d7b9c4b 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java
@@ -35,6 +35,7 @@ import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
+import org.eclipse.leshan.core.node.codec.NodeEncoder;
 import org.eclipse.leshan.core.tlv.Tlv;
 import org.eclipse.leshan.core.tlv.Tlv.TlvType;
 import org.eclipse.leshan.core.tlv.TlvEncoder;
@@ -45,11 +46,12 @@ import org.slf4j.LoggerFactory;
 /**
  * TLV encoder for {@link LwM2mNode}.
  */
-public class LwM2mNodeTlvEncoder {
+public class LwM2mNodeTlvEncoder implements NodeEncoder {
 
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeTlvEncoder.class);
 
-    public static byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter)
+    @Override
+    public byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter)
             throws CodecException {
         Validate.notNull(node);
         Validate.notNull(path);
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/util/json/JacksonJsonSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/core/util/json/JacksonJsonSerDes.java
new file mode 100644
index 00000000..092cb1db
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/util/json/JacksonJsonSerDes.java
@@ -0,0 +1,69 @@
+/*******************************************************************************
+ * Copyright (c) 2017 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.util.json;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+
+/**
+ * An abstract class to easily create serializer/deserializer class based on "minimal-json".
+ * 
+ * @param <T> the type of the objects to serialize or deserialize.
+ */
+public abstract class JacksonJsonSerDes<T> {
+
+    public abstract JsonNode jSerialize(T t) throws JsonException;
+
+    public String sSerialize(T t) throws JsonException {
+        return jSerialize(t).toString();
+    }
+
+    public byte[] bSerialize(T t) throws JsonException {
+        return jSerialize(t).toString().getBytes();
+    }
+
+    public ArrayNode jSerialize(Collection<T> jaes) throws JsonException {
+        ArrayNode array = JsonNodeFactory.instance.arrayNode();
+        for (T jae : jaes) {
+            array.add(jSerialize(jae));
+        }
+        return array;
+    }
+
+    public String sSerialize(Collection<T> ts) throws JsonException {
+        return jSerialize(ts).toString();
+    }
+
+    public byte[] bSerialize(Collection<T> ts) throws JsonException {
+        return jSerialize(ts).toString().getBytes();
+    }
+
+    public abstract T deserialize(JsonNode o) throws JsonException;
+
+    public List<T> deserialize(Iterator<JsonNode> nodes) throws JsonException {
+        ArrayList<T> res = new ArrayList<>();
+        while (nodes.hasNext()) {
+            res.add(deserialize(nodes.next()));
+        }
+        return res;
+    }
+}
\ No newline at end of file
diff --git a/pom.xml b/pom.xml
index 51f0b18b..0b1924ed 100644
--- a/pom.xml
+++ b/pom.xml
@@ -690,6 +690,17 @@ Contributors:
                 <artifactId>minimal-json</artifactId>
                 <version>0.9.5</version>
             </dependency>
+            <dependency>
+                <groupId>com.fasterxml.jackson.core</groupId>
+                <artifactId>jackson-databind</artifactId>
+                <version>2.9.9</version>
+            </dependency>
+            <dependency>
+                <groupId>com.fasterxml.jackson.dataformat</groupId>
+                <artifactId>jackson-dataformat-cbor</artifactId>
+                <version>2.9.9</version>
+            </dependency>
+            
 
             <!-- Demos, examples and tests dependencies -->
             <dependency>
@@ -707,11 +718,6 @@ Contributors:
                 <artifactId>gson</artifactId>
                 <version>2.8.6</version>
             </dependency>
-            <dependency>
-                <groupId>com.fasterxml.jackson.dataformat</groupId>
-                <artifactId>jackson-dataformat-cbor</artifactId>
-                <version>2.9.9</version>
-            </dependency>
             <dependency>
                 <groupId>commons-lang</groupId>
                 <artifactId>commons-lang</artifactId>


Test patch:
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/json/JsonDeserializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/json/JsonDeserializerTest.java
index 0cb8b023..4f3ac912 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/json/JsonDeserializerTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/json/JsonDeserializerTest.java
@@ -16,9 +16,7 @@
  *******************************************************************************/
 package org.eclipse.leshan.core.json;
 
-import org.eclipse.leshan.core.json.JsonRootObject;
-import org.eclipse.leshan.core.json.LwM2mJson;
-import org.eclipse.leshan.core.json.LwM2mJsonException;
+import org.eclipse.leshan.core.json.jackson.LwM2mJsonJacksonEncoderDecoder;
 import org.junit.Assert;
 import org.junit.Test;
 import org.slf4j.Logger;
@@ -26,7 +24,9 @@ import org.slf4j.LoggerFactory;
 
 public class JsonDeserializerTest {
 
-    private Logger log = LoggerFactory.getLogger(getClass());
+    private static final Logger log = LoggerFactory.getLogger(JsonDeserializerTest.class);
+
+    private static final LwM2mJsonJacksonEncoderDecoder LwM2mJson = new LwM2mJsonJacksonEncoderDecoder();
 
     @Test
     public void deserialize_device_object() throws LwM2mJsonException {
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/json/JsonSerializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/json/JsonSerializerTest.java
index ad246230..76a165c7 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/json/JsonSerializerTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/json/JsonSerializerTest.java
@@ -18,10 +18,7 @@ package org.eclipse.leshan.core.json;
 
 import java.util.ArrayList;
 
-import org.eclipse.leshan.core.json.JsonArrayEntry;
-import org.eclipse.leshan.core.json.JsonRootObject;
-import org.eclipse.leshan.core.json.LwM2mJson;
-import org.eclipse.leshan.core.json.LwM2mJsonException;
+import org.eclipse.leshan.core.json.jackson.LwM2mJsonJacksonEncoderDecoder;
 import org.junit.Assert;
 import org.junit.Test;
 import org.slf4j.Logger;
@@ -29,7 +26,9 @@ import org.slf4j.LoggerFactory;
 
 public class JsonSerializerTest {
 
-    private Logger LOG = LoggerFactory.getLogger(getClass());
+    private static Logger LOG = LoggerFactory.getLogger(JsonSerializerTest.class);
+
+    private static final LwM2mJsonJacksonEncoderDecoder LwM2mJson = new LwM2mJsonJacksonEncoderDecoder();
 
     @Test
     public void serialize_device_object() throws LwM2mJsonException {
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java
index 9cbf4e93..2f98bda7 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeEncoderTest.java
@@ -195,7 +195,7 @@ public class LwM2mNodeEncoderTest {
         b.append("{\"n\":\"9\",\"v\":100},");
         b.append("{\"n\":\"10\",\"v\":15},");
         b.append("{\"n\":\"11/0\",\"v\":0},");
-        b.append("{\"n\":\"13\",\"v\":1.367491215E9},");
+        b.append("{\"n\":\"13\",\"v\":1367491215},");
         b.append("{\"n\":\"14\",\"sv\":\"+02:00\"},");
         b.append("{\"n\":\"16\",\"sv\":\"U\"}]}");
 
@@ -293,7 +293,7 @@ public class LwM2mNodeEncoderTest {
         b.append("{\"n\":\"0/1\",\"v\":22.9,\"t\":210},");
         b.append("{\"n\":\"0/0\",\"sv\":\"a string\",\"t\":220},");
         b.append("{\"n\":\"0/1\",\"v\":22.4,\"t\":220},");
-        b.append("{\"n\":\"1/1\",\"v\":23,\"t\":220},");
+        b.append("{\"n\":\"1/1\",\"v\":23.0,\"t\":220},");
         b.append("{\"n\":\"0/1\",\"v\":24.1,\"t\":230}]}");
 
         String expected = b.toString();
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java
index 938f1dc0..7bdb9d95 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java
@@ -325,7 +325,7 @@ public class ObserveTest {
         assertTrue("New observation is not there", observations.contains(observation));
 
         // *** HACK send a notification with unsupported content format *** //
-        byte[] payload = LwM2mNodeJsonEncoder.encode(LwM2mSingleResource.newStringResource(15, "Paris"),
+        byte[] payload = new LwM2mNodeJsonEncoder().encode(LwM2mSingleResource.newStringResource(15, "Paris"),
                 new LwM2mPath("/3/0/15"), new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
         Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
         sendNotification(getConnector(helper.client, helper.getCurrentRegisteredServer()), payload, firstCoapResponse,
@@ -368,7 +368,7 @@ public class ObserveTest {
         timestampedNodes.add(mostRecentNode);
         timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
                 LwM2mSingleResource.newStringResource(15, "Londres")));
-        byte[] payload = LwM2mNodeJsonEncoder.encodeTimestampedData(timestampedNodes, new LwM2mPath("/3/0/15"),
+        byte[] payload = new LwM2mNodeJsonEncoder().encodeTimestampedData(timestampedNodes, new LwM2mPath("/3/0/15"),
                 new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
         Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
         sendNotification(getConnector(helper.client, helper.getCurrentRegisteredServer()), payload, firstCoapResponse,
@@ -412,7 +412,7 @@ public class ObserveTest {
         timestampedNodes.add(mostRecentNode);
         timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
                 new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres"))));
-        byte[] payload = LwM2mNodeJsonEncoder.encodeTimestampedData(timestampedNodes, new LwM2mPath("/3/0"),
+        byte[] payload = new LwM2mNodeJsonEncoder().encodeTimestampedData(timestampedNodes, new LwM2mPath("/3/0"),
                 new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
         Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
         sendNotification(getConnector(helper.client, helper.getCurrentRegisteredServer()), payload, firstCoapResponse,
@@ -456,7 +456,7 @@ public class ObserveTest {
         timestampedNodes.add(mostRecentNode);
         timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
                 new LwM2mObject(3, new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres")))));
-        byte[] payload = LwM2mNodeJsonEncoder.encodeTimestampedData(timestampedNodes, new LwM2mPath("/3"),
+        byte[] payload = new LwM2mNodeJsonEncoder().encodeTimestampedData(timestampedNodes, new LwM2mPath("/3"),
                 new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
 
         Response firstCoapResponse = (Response) observeResponse.getCoapResponse();