Repository: eclipse/leshan
Pull request #150 (2016-06-29 09:54:07)

Timestamped notification
Add support of timestamped notification in the API and for JSON format at server side.
Add a way to add a custom encoder/decoder to support more content format.


Patch:
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
index 09af3aa7..fcb8e6b7 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/LeshanClient.java
@@ -44,6 +44,8 @@ import org.eclipse.leshan.client.servers.BootstrapHandler;
 import org.eclipse.leshan.client.servers.DmServerInfo;
 import org.eclipse.leshan.client.servers.RegistrationEngine;
 import org.eclipse.leshan.client.servers.ServerInfo;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
 import org.eclipse.leshan.util.Validate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -144,7 +146,8 @@ public class LeshanClient implements LwM2mClient {
 
         // Create CoAP resources for each lwm2m Objects.
         for (LwM2mObjectEnabler enabler : objectEnablers) {
-            final ObjectResource clientObject = new ObjectResource(enabler, bootstrapHandler);
+            final ObjectResource clientObject = new ObjectResource(enabler, bootstrapHandler,
+                    new DefaultLwM2mNodeEncoder(), new DefaultLwM2mNodeDecoder());
             clientSideServer.add(clientObject);
         }
 
diff --git a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/impl/ObjectResource.java b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/impl/ObjectResource.java
index 3bbdda79..d17a7328 100644
--- a/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/impl/ObjectResource.java
+++ b/leshan-client-cf/src/main/java/org/eclipse/leshan/client/californium/impl/ObjectResource.java
@@ -77,12 +77,17 @@ public class ObjectResource extends CoapResource implements NotifySender {
 
     private final LwM2mObjectEnabler nodeEnabler;
     private final BootstrapHandler bootstrapHandler;
+    private final LwM2mNodeEncoder encoder;
+    private final LwM2mNodeDecoder decoder;
 
-    public ObjectResource(LwM2mObjectEnabler nodeEnabler, BootstrapHandler bootstrapHandler) {
+    public ObjectResource(LwM2mObjectEnabler nodeEnabler, BootstrapHandler bootstrapHandler, LwM2mNodeEncoder encoder,
+            LwM2mNodeDecoder decoder) {
         super(Integer.toString(nodeEnabler.getId()));
         this.nodeEnabler = nodeEnabler;
         this.nodeEnabler.setNotifySender(this);
         this.bootstrapHandler = bootstrapHandler;
+        this.encoder = encoder;
+        this.decoder = decoder;
         setObservable(true);
     }
 
@@ -115,7 +120,7 @@ public class ObjectResource extends CoapResource implements NotifySender {
             ContentFormat format = ContentFormat.TLV; // use TLV as default format
             if (exchange.getRequestOptions().hasAccept()) {
                 format = ContentFormat.fromCode(exchange.getRequestOptions().getAccept());
-                if (format == null) {
+                if (!encoder.isSupported(format)) {
                     exchange.respond(ResponseCode.NOT_ACCEPTABLE);
                     return;
                 }
@@ -128,7 +133,7 @@ public class ObjectResource extends CoapResource implements NotifySender {
                     LwM2mPath path = new LwM2mPath(URI);
                     LwM2mNode content = response.getContent();
                     LwM2mModel model = new LwM2mModel(nodeEnabler.getObjectModel());
-                    exchange.respond(ResponseCode.CONTENT, LwM2mNodeEncoder.encode(content, format, path, model),
+                    exchange.respond(ResponseCode.CONTENT, encoder.encode(content, format, path, model),
                             format.getCode());
                     return;
                 } else {
@@ -143,7 +148,7 @@ public class ObjectResource extends CoapResource implements NotifySender {
                     LwM2mPath path = new LwM2mPath(URI);
                     LwM2mNode content = response.getContent();
                     LwM2mModel model = new LwM2mModel(nodeEnabler.getObjectModel());
-                    exchange.respond(ResponseCode.CONTENT, LwM2mNodeEncoder.encode(content, format, path, model),
+                    exchange.respond(ResponseCode.CONTENT, encoder.encode(content, format, path, model),
                             format.getCode());
                     return;
                 } else {
@@ -177,14 +182,14 @@ public class ObjectResource extends CoapResource implements NotifySender {
         else {
             LwM2mPath path = new LwM2mPath(URI);
             ContentFormat contentFormat = ContentFormat.fromCode(coapExchange.getRequestOptions().getContentFormat());
-            if (contentFormat == null) {
+            if (!decoder.isSupported(contentFormat)) {
                 coapExchange.respond(ResponseCode.UNSUPPORTED_CONTENT_FORMAT);
                 return;
             }
             LwM2mNode lwM2mNode;
             try {
                 LwM2mModel model = new LwM2mModel(nodeEnabler.getObjectModel());
-                lwM2mNode = LwM2mNodeDecoder.decode(coapExchange.getRequestPayload(), contentFormat, path, model);
+                lwM2mNode = decoder.decode(coapExchange.getRequestPayload(), contentFormat, path, model);
                 if (identity.isLwm2mBootstrapServer()) {
                     BootstrapWriteResponse response = nodeEnabler.write(identity,
                             new BootstrapWriteRequest(path, lwM2mNode, contentFormat));
@@ -222,7 +227,7 @@ public class ObjectResource extends CoapResource implements NotifySender {
 
         // handle content format for Write (Update) and Create request
         ContentFormat contentFormat = ContentFormat.fromCode(exchange.getRequestOptions().getContentFormat());
-        if (contentFormat == null) {
+        if (!decoder.isSupported(contentFormat)) {
             exchange.respond(ResponseCode.UNSUPPORTED_CONTENT_FORMAT);
             return;
         }
@@ -231,7 +236,7 @@ public class ObjectResource extends CoapResource implements NotifySender {
         // Manage Update Instance
         if (path.isObjectInstance()) {
             try {
-                LwM2mNode lwM2mNode = LwM2mNodeDecoder.decode(exchange.getRequestPayload(), contentFormat, path, model);
+                LwM2mNode lwM2mNode = decoder.decode(exchange.getRequestPayload(), contentFormat, path, model);
                 WriteResponse response = nodeEnabler.write(identity,
                         new WriteRequest(Mode.UPDATE, contentFormat, URI, lwM2mNode));
                 exchange.respond(fromLwM2mCode(response.getCode()), response.getErrorMessage());
@@ -245,7 +250,7 @@ public class ObjectResource extends CoapResource implements NotifySender {
         // Manage Create Request
         try {
             // decode the payload as an instance
-            LwM2mObjectInstance newInstance = LwM2mNodeDecoder.decode(exchange.getRequestPayload(), contentFormat,
+            LwM2mObjectInstance newInstance = decoder.decode(exchange.getRequestPayload(), contentFormat,
                     new LwM2mPath(path.getObjectId()), model, LwM2mObjectInstance.class);
 
             if (newInstance.getResources().isEmpty()) {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java
index f793b956..03e9db18 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mPath.java
@@ -96,7 +96,7 @@ public class LwM2mPath {
      * @param path the path (e.g. "/3/0/1" or "/3")
      */
     public LwM2mPath(String path) {
-        Validate.notEmpty(path);
+        Validate.notNull(path);
         if (path.startsWith("/")) {
             path = path.substring(1);
         }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/TimestampedLwM2mNode.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/TimestampedLwM2mNode.java
new file mode 100644
index 00000000..abc1572e
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/TimestampedLwM2mNode.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2016 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.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;
+
+import org.eclipse.leshan.util.Validate;
+
+public class TimestampedLwM2mNode {
+
+    private final Long timestamp;
+
+    private final LwM2mNode node;
+
+    public TimestampedLwM2mNode(Long timestamp, LwM2mNode node) {
+        Validate.notNull(node);
+        this.timestamp = timestamp;
+        this.node = node;
+    }
+
+    public Long getTimestamp() {
+        return timestamp;
+    }
+
+    public LwM2mNode getNode() {
+        return node;
+    }
+
+    public boolean isTimespamped() {
+        return timestamp != null && timestamp >= 0;
+    }
+}
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
new file mode 100644
index 00000000..73eb1228
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
@@ -0,0 +1,158 @@
+/*******************************************************************************
+ * Copyright (c) 2013-2015 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.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.node.codec;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.model.ResourceModel;
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.LwM2mObject;
+import org.eclipse.leshan.core.node.LwM2mObjectInstance;
+import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
+import org.eclipse.leshan.core.node.codec.json.LwM2mNodeJsonDecoder;
+import org.eclipse.leshan.core.node.codec.opaque.LwM2mNodeOpaqueDecoder;
+import org.eclipse.leshan.core.node.codec.text.LwM2mNodeTextDecoder;
+import org.eclipse.leshan.core.node.codec.tlv.LwM2mNodeTlvDecoder;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.util.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
+
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultLwM2mNodeDecoder.class);
+
+    @Override
+    public LwM2mNode decode(byte[] content, ContentFormat format, LwM2mPath path, LwM2mModel model)
+            throws InvalidValueException {
+        return decode(content, format, path, model, nodeClassFromPath(path));
+    }
+
+    private ContentFormat guessContentType(LwM2mPath path, LwM2mModel model) {
+        ContentFormat format;
+        if (path.isResource()) {
+            ResourceModel rDesc = model.getResourceModel(path.getObjectId(), path.getResourceId());
+            if (rDesc != null && rDesc.multiple) {
+                format = ContentFormat.TLV;
+            } else {
+                if (rDesc != null && rDesc.type == Type.OPAQUE) {
+                    format = ContentFormat.OPAQUE;
+                } else {
+                    format = ContentFormat.TEXT;
+                }
+            }
+        } else {
+            format = ContentFormat.TLV;
+        }
+        return format;
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T extends LwM2mNode> T decode(byte[] content, ContentFormat format, LwM2mPath path, LwM2mModel model,
+            Class<T> nodeClass) throws InvalidValueException {
+
+        LOG.debug("Decoding value for path {} and format {}: {}", path, format, content);
+        Validate.notNull(path);
+
+        // If no format is given, guess the best one to use.
+        if (format == null) {
+            format = guessContentType(path, model);
+        }
+
+        // Decode content.
+        switch (format.getCode()) {
+        case ContentFormat.TEXT_CODE:
+            return (T) LwM2mNodeTextDecoder.decode(content, path, model);
+        case ContentFormat.TLV_CODE:
+            return LwM2mNodeTlvDecoder.decode(content, path, model, nodeClass);
+        case ContentFormat.OPAQUE_CODE:
+            return (T) LwM2mNodeOpaqueDecoder.decode(content, path, model);
+        case ContentFormat.JSON_CODE:
+            return LwM2mNodeJsonDecoder.decode(content, path, model, nodeClass);
+        case ContentFormat.LINK_CODE:
+            throw new UnsupportedOperationException("Content format " + format + " not yet implemented '" + path + "'");
+        }
+        return null;
+    }
+
+    @Override
+    public List<TimestampedLwM2mNode> decodeTimestampedData(byte[] content, ContentFormat format, LwM2mPath path,
+            LwM2mModel model) throws InvalidValueException {
+        LOG.debug("Decoding value for path {} and format {}: {}", path, format, content);
+        Validate.notNull(path);
+
+        // If no format is given, guess the best one to use.
+        if (format == null) {
+            format = guessContentType(path, model);
+        }
+
+        // Decode content.
+        switch (format.getCode()) {
+        case ContentFormat.TEXT_CODE:
+            return toTimestampedNodes(LwM2mNodeTextDecoder.decode(content, path, model));
+        case ContentFormat.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:
+            return LwM2mNodeJsonDecoder.decodeTimestamped(content, path, model, nodeClassFromPath(path));
+        case ContentFormat.LINK_CODE:
+            throw new UnsupportedOperationException("Content format " + format + " not yet implemented '" + path + "'");
+        }
+        return null;
+    }
+
+    private static List<TimestampedLwM2mNode> toTimestampedNodes(LwM2mNode node) {
+        if (node == null)
+            return Collections.emptyList();
+
+        ArrayList<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>(1);
+        timestampedNodes.add(new TimestampedLwM2mNode(null, node));
+        return Collections.unmodifiableList(timestampedNodes);
+    }
+
+    public static Class<? extends LwM2mNode> nodeClassFromPath(LwM2mPath path) {
+        if (path.isObject()) {
+            return LwM2mObject.class;
+        } else if (path.isObjectInstance()) {
+            return LwM2mObjectInstance.class;
+        } else if (path.isResource()) {
+            return LwM2mResource.class;
+        }
+        throw new IllegalArgumentException("invalid path level: " + path);
+    }
+
+    @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;
+        default:
+            return false;
+        }
+    }
+}
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
new file mode 100644
index 00000000..4b180bab
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
@@ -0,0 +1,101 @@
+/*******************************************************************************
+ * Copyright (c) 2013-2015 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v10.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.node.codec;
+
+import java.util.Arrays;
+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;
+import org.eclipse.leshan.core.node.codec.json.LwM2mNodeJsonEncoder;
+import org.eclipse.leshan.core.node.codec.opaque.LwM2mNodeOpaqueEncoder;
+import org.eclipse.leshan.core.node.codec.text.LwM2mNodeTextEncoder;
+import org.eclipse.leshan.core.node.codec.tlv.LwM2mNodeTlvEncoder;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.util.Validate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultLwM2mNodeEncoder implements LwM2mNodeEncoder {
+
+    private static final Logger LOG = LoggerFactory.getLogger(DefaultLwM2mNodeEncoder.class);
+
+    @Override
+    public byte[] encode(LwM2mNode node, ContentFormat format, LwM2mPath path, LwM2mModel model) {
+        Validate.notNull(node);
+        Validate.notNull(format);
+
+        LOG.debug("Encoding node {} for path {} and format {}", node, path, format);
+
+        byte[] encoded = null;
+        switch (format.getCode()) {
+        case ContentFormat.TLV_CODE:
+            encoded = LwM2mNodeTlvEncoder.encode(node, path, model);
+            break;
+        case ContentFormat.TEXT_CODE:
+            encoded = LwM2mNodeTextEncoder.encode(node, path, model);
+            break;
+        case ContentFormat.OPAQUE_CODE:
+            encoded = LwM2mNodeOpaqueEncoder.encode(node, path, model);
+            break;
+        case ContentFormat.JSON_CODE:
+            encoded = LwM2mNodeJsonEncoder.encode(node, path, model);
+            break;
+        default:
+            throw new IllegalArgumentException("Cannot encode " + node + " with format " + format);
+        }
+
+        LOG.trace("Encoded node {}: {}", node, Arrays.toString(encoded));
+        return encoded;
+    }
+
+    @Override
+    public byte[] encodeTimestampedData(List<TimestampedLwM2mNode> timestampedNodes, ContentFormat format,
+            LwM2mPath path, LwM2mModel model) {
+        Validate.notEmpty(timestampedNodes);
+        Validate.notNull(format);
+
+        LOG.debug("Encoding time-stamped nodes for path {} and format {}", timestampedNodes, path, format);
+
+        byte[] encoded = null;
+        switch (format.getCode()) {
+        case ContentFormat.JSON_CODE:
+            encoded = LwM2mNodeJsonEncoder.encodeTimestampedData(timestampedNodes, path, model);
+            break;
+        default:
+            throw new IllegalArgumentException("Cannot encode timestampedNode with format " + format);
+        }
+
+        LOG.trace("Encoded node timestampedNode: {}", timestampedNodes, Arrays.toString(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:
+            return true;
+        default:
+            return false;
+        }
+    }
+}
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 5a4c8b23..0acf7462 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
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2013-2015 Sierra Wireless and others.
+ * Copyright (c) 2016 Sierra Wireless and others.
  * 
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
@@ -12,30 +12,18 @@
  * 
  * Contributors:
  *     Sierra Wireless - initial API and implementation
- *     Gemalto M2M GmbH
  *******************************************************************************/
 package org.eclipse.leshan.core.node.codec;
 
+import java.util.List;
+
 import org.eclipse.leshan.core.model.LwM2mModel;
-import org.eclipse.leshan.core.model.ResourceModel;
-import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.node.LwM2mNode;
-import org.eclipse.leshan.core.node.LwM2mObject;
-import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
-import org.eclipse.leshan.core.node.LwM2mResource;
-import org.eclipse.leshan.core.node.codec.json.LwM2mNodeJsonDecoder;
-import org.eclipse.leshan.core.node.codec.opaque.LwM2mNodeOpaqueDecoder;
-import org.eclipse.leshan.core.node.codec.text.LwM2mNodeTextDecoder;
-import org.eclipse.leshan.core.node.codec.tlv.LwM2mNodeTlvDecoder;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.request.ContentFormat;
-import org.eclipse.leshan.util.Validate;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class LwM2mNodeDecoder {
 
-    private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeDecoder.class);
+public interface LwM2mNodeDecoder {
 
     /**
      * Deserializes a binary content into a {@link LwM2mNode}.
@@ -49,10 +37,8 @@ public class LwM2mNodeDecoder {
      * @return the resulting node
      * @throws InvalidValueException
      */
-    public static LwM2mNode decode(byte[] content, ContentFormat format, LwM2mPath path, LwM2mModel model)
-            throws InvalidValueException {
-        return decode(content, format, path, model, nodeClassFromPath(path));
-    }
+    LwM2mNode decode(byte[] content, ContentFormat format, LwM2mPath path, LwM2mModel model)
+            throws InvalidValueException;
 
     /**
      * Deserializes a binary content into a {@link LwM2mNode} of the expected type.
@@ -65,55 +51,26 @@ public class LwM2mNodeDecoder {
      * @return the resulting node
      * @throws InvalidValueException
      */
-    @SuppressWarnings("unchecked")
-    public static <T extends LwM2mNode> T decode(byte[] content, ContentFormat format, LwM2mPath path, LwM2mModel model,
-            Class<T> nodeClass) throws InvalidValueException {
+    <T extends LwM2mNode> T decode(byte[] content, ContentFormat format, LwM2mPath path, LwM2mModel model,
+            Class<T> nodeClass) throws InvalidValueException;
 
-        LOG.debug("Decoding value for path {} and format {}: {}", path, format, content);
-        Validate.notNull(path);
-
-        // If no format is given, guess the best one to use.
-        if (format == null) {
-            if (path.isResource()) {
-                ResourceModel rDesc = model.getResourceModel(path.getObjectId(), path.getResourceId());
-                if (rDesc != null && rDesc.multiple) {
-                    format = ContentFormat.TLV;
-                } else {
-                    if (rDesc != null && rDesc.type == Type.OPAQUE) {
-                        format = ContentFormat.OPAQUE;
-                    } else {
-                        format = ContentFormat.TEXT;
-                    }
-                }
-            } else {
-                format = ContentFormat.TLV;
-            }
-        }
+    /**
+     * Deserializes a binary content into a list of time-stamped {@link LwM2mNode} ordering by time-stamp.
+     *
+     * @param content the content
+     * @param format the content format
+     * @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
+     * @throws InvalidValueException
+     */
+    List<TimestampedLwM2mNode> decodeTimestampedData(byte[] content, ContentFormat format, LwM2mPath path,
+            LwM2mModel model) throws InvalidValueException;
 
-        // Decode content.
-        switch (format) {
-        case TEXT:
-            return (T) LwM2mNodeTextDecoder.decode(content, path, model);
-        case TLV:
-            return LwM2mNodeTlvDecoder.decode(content, path, model, nodeClass);
-        case OPAQUE:
-            return (T) LwM2mNodeOpaqueDecoder.decode(content, path, model);
-        case JSON:
-            return LwM2mNodeJsonDecoder.decode(content, path, model, nodeClass);
-        case LINK:
-            throw new UnsupportedOperationException("Content format " + format + " not yet implemented '" + path + "'");
-        }
-        return null;
-    }
+    /**
+     * return true is the given {@link ContentFormat} is supported
+     */
+    boolean isSupported(ContentFormat format);
 
-    private static Class<? extends LwM2mNode> nodeClassFromPath(LwM2mPath path) {
-        if (path.isObject()) {
-            return LwM2mObject.class;
-        } else if (path.isObjectInstance()) {
-            return LwM2mObjectInstance.class;
-        } else if (path.isResource()) {
-            return LwM2mResource.class;
-        }
-        throw new IllegalArgumentException("invalid path level: " + path);
-    }
 }
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 48de2f25..62323190 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
@@ -1,5 +1,5 @@
 /*******************************************************************************
- * Copyright (c) 2013-2015 Sierra Wireless and others.
+ * Copyright (c) 2016 Sierra Wireless and others.
  * 
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
@@ -12,27 +12,18 @@
  * 
  * Contributors:
  *     Sierra Wireless - initial API and implementation
- *     Gemalto M2M GmbH
  *******************************************************************************/
 package org.eclipse.leshan.core.node.codec;
 
-import java.util.Arrays;
+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.codec.json.LwM2mNodeJsonEncoder;
-import org.eclipse.leshan.core.node.codec.opaque.LwM2mNodeOpaqueEncoder;
-import org.eclipse.leshan.core.node.codec.text.LwM2mNodeTextEncoder;
-import org.eclipse.leshan.core.node.codec.tlv.LwM2mNodeTlvEncoder;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.request.ContentFormat;
-import org.eclipse.leshan.util.Validate;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeEncoder {
-
-    private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeEncoder.class);
+public interface LwM2mNodeEncoder {
 
     /**
      * Serializes a {@link LwM2mNode} with the given content format.
@@ -43,32 +34,22 @@ public class LwM2mNodeEncoder {
      * @param model the collection of supported object models
      * @return the encoded node as a byte array
      */
-    public static byte[] encode(LwM2mNode node, ContentFormat format, LwM2mPath path, LwM2mModel model) {
-        Validate.notNull(node);
-        Validate.notNull(format);
-
-        LOG.debug("Encoding node {} for path {} and format {}", node, path, format);
-
-        byte[] encoded = null;
-        switch (format) {
-        case TLV:
-            encoded = LwM2mNodeTlvEncoder.encode(node, path, model);
-            break;
-        case TEXT:
-            encoded = LwM2mNodeTextEncoder.encode(node, path, model);
-            break;
-        case OPAQUE:
-            encoded = LwM2mNodeOpaqueEncoder.encode(node, path, model);
-            break;
-        case JSON:
-            encoded = LwM2mNodeJsonEncoder.encode(node, path, model);
-            break;
-        default:
-            throw new IllegalArgumentException("Cannot encode " + node + " with format " + format);
-        }
+    byte[] encode(LwM2mNode node, ContentFormat format, LwM2mPath path, LwM2mModel model);
 
-        LOG.trace("Encoded node {}: {}", node, Arrays.toString(encoded));
-        return encoded;
-    }
+    /**
+     * Serializes a list of time-stamped {@link LwM2mNode} with the given content format.
+     *
+     * @param timestampedNodes the list of time-stamped object/instance/resource to serialize
+     * @param format the content format
+     * @param path the path of the node to serialize
+     * @param model the collection of supported object models
+     * @return the encoded node as a byte array
+     */
+    byte[] encodeTimestampedData(List<TimestampedLwM2mNode> timestampedNodes, ContentFormat format, LwM2mPath path,
+            LwM2mModel model);
 
+    /**
+     * return true is the given {@link ContentFormat} is supported
+     */
+    boolean isSupported(ContentFormat format);
 }
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 f65c7c90..f8d687df 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
@@ -15,10 +15,17 @@
  *******************************************************************************/
 package org.eclipse.leshan.core.node.codec.json;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.SortedMap;
+import java.util.TreeMap;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ResourceModel;
@@ -30,6 +37,7 @@ import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.node.codec.InvalidValueException;
 import org.eclipse.leshan.json.JsonArrayEntry;
 import org.eclipse.leshan.json.JsonRootObject;
@@ -43,8 +51,26 @@ public class LwM2mNodeJsonDecoder {
 
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeJsonDecoder.class);
 
+    @SuppressWarnings("unchecked")
     public static <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
             throws InvalidValueException {
+        try {
+            String jsonStrValue = new String(content);
+            JsonRootObject json = LwM2mJson.fromJsonLwM2m(jsonStrValue);
+            List<TimestampedLwM2mNode> timestampedNodes = parseJSON(json, path, model, nodeClass);
+            if (timestampedNodes.size() == 0) {
+                return null;
+            } else {
+                // return the most recent value
+                return (T) timestampedNodes.get(0).getNode();
+            }
+        } catch (LwM2mJsonException e) {
+            throw new InvalidValueException("Unable to deSerialize json", path, e);
+        }
+    }
+
+    public static List<TimestampedLwM2mNode> decodeTimestamped(byte[] content, LwM2mPath path, LwM2mModel model,
+            Class<? extends LwM2mNode> nodeClass) throws InvalidValueException {
         try {
             String jsonStrValue = new String(content);
             JsonRootObject json = LwM2mJson.fromJsonLwM2m(jsonStrValue);
@@ -54,36 +80,168 @@ public class LwM2mNodeJsonDecoder {
         }
     }
 
-    @SuppressWarnings("unchecked")
-    private static <T extends LwM2mNode> T parseJSON(JsonRootObject jsonObject, LwM2mPath path, LwM2mModel model,
-            Class<T> nodeClass) throws InvalidValueException {
+    private static List<TimestampedLwM2mNode> parseJSON(JsonRootObject jsonObject, LwM2mPath path, LwM2mModel model,
+            Class<? extends LwM2mNode> nodeClass) throws InvalidValueException {
+
         LOG.trace("Parsing JSON content for path {}: {}", path, jsonObject);
 
-        if (nodeClass == LwM2mObject.class) {
-            // TODO
-            // If bn is present will have multiple object instances in JSON payload
-            // If JSON contains ObjLnk -> this method should return List<LwM2mNode> ?
-            throw new UnsupportedOperationException("JSON object level decoding is not implemented");
-        } else if (nodeClass == LwM2mObjectInstance.class) {
-            // object instance level request,
-            LwM2mPath baseName = extractAndValidateBaseName(jsonObject, path);
-            Map<Integer, LwM2mResource> resourceMap = parseJsonPayLoadLwM2mResources(jsonObject, path, baseName, model);
-            // try to find instance Id
-            int objectInstanceID = LwM2mObjectInstance.UNDEFINED;
-            if (path.getObjectInstanceId() != null) {
-                objectInstanceID = path.getObjectInstanceId();
-            } else if (baseName != null && baseName.getObjectInstanceId() != null) {
-                objectInstanceID = baseName.getObjectInstanceId();
+        // Group JSON entry by time-stamp
+        Map<Long, Collection<JsonArrayEntry>> jsonEntryByTimestamp = groupJsonEntryByTimestamp(jsonObject);
+
+        // Extract baseName
+        LwM2mPath baseName = extractAndValidateBaseName(jsonObject, path);
+        if (baseName == null)
+            baseName = path; // if no base name, use request path as base name
+
+        // fill time-stamped nodes collection
+        List<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>();
+        for (Entry<Long, Collection<JsonArrayEntry>> entryByTimestamp : jsonEntryByTimestamp.entrySet()) {
+
+            // Group JSON entry by instance
+            Map<Integer, Collection<JsonArrayEntry>> jsonEntryByInstanceId = groupJsonEntryByInstanceId(
+                    entryByTimestamp.getValue(), baseName);
+
+            // Create lwm2m node
+            LwM2mNode node = null;
+            if (nodeClass == LwM2mObject.class) {
+                Collection<LwM2mObjectInstance> instances = new ArrayList<>();
+                for (Entry<Integer, Collection<JsonArrayEntry>> entryByInstanceId : jsonEntryByInstanceId.entrySet()) {
+                    Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(entryByInstanceId.getValue(),
+                            baseName, model);
+
+                    instances.add(new LwM2mObjectInstance(entryByInstanceId.getKey(), resourcesMap.values()));
+                }
+
+                node = new LwM2mObject(baseName.getObjectId(), instances);
+            } else if (nodeClass == LwM2mObjectInstance.class) {
+                // validate we have resources for only 1 instance
+                if (jsonEntryByInstanceId.size() > 1)
+                    throw new InvalidValueException("Only one instance expected in the payload", path);
+
+                // Extract resources
+                Entry<Integer, Collection<JsonArrayEntry>> instanceEntry = jsonEntryByInstanceId.entrySet().iterator()
+                        .next();
+                Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(instanceEntry.getValue(), baseName,
+                        model);
+
+                // Create instance
+                node = new LwM2mObjectInstance(instanceEntry.getKey(), resourcesMap.values());
+            } else if (nodeClass == LwM2mResource.class) {
+                // validate we have resources for only 1 instance
+                if (jsonEntryByInstanceId.size() > 1)
+                    throw new InvalidValueException("Only one instance expected in the payload", path);
+
+                // Extract resources
+                Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(jsonEntryByInstanceId.values()
+                        .iterator().next(), baseName, model);
+
+                // validate there is only 1 resource
+                if (resourcesMap.size() != 1)
+                    throw new InvalidValueException("Only one resource should be present in the payload", path);
+
+                node = resourcesMap.values().iterator().next();
+            } else {
+                throw new IllegalArgumentException("invalid node class: " + nodeClass);
+            }
+
+            // compute time-stamp
+            Long timestamp = computeTimestamp(jsonObject.getBaseTime(), entryByTimestamp.getKey());
+
+            // add time-stamped node
+            timestampedNodes.add(new TimestampedLwM2mNode(timestamp, node));
+        }
+
+        return timestampedNodes;
+
+    }
+
+    private static Long computeTimestamp(Long baseTime, Long time) {
+        Long timestamp;
+        if (baseTime != null) {
+            if (time != null) {
+                timestamp = baseTime + time;
+            } else {
+                timestamp = baseTime;
             }
-            return (T) new LwM2mObjectInstance(objectInstanceID, resourceMap.values());
-        } else if (nodeClass == LwM2mResource.class) {
-            // resource level request
-            LwM2mPath baseName = extractAndValidateBaseName(jsonObject, path);
-            Map<Integer, LwM2mResource> resourceMap = parseJsonPayLoadLwM2mResources(jsonObject, path, baseName, model);
-            return (T) resourceMap.values().iterator().next();
         } else {
-            throw new IllegalArgumentException("invalid node class: " + nodeClass);
+            if (time != null) {
+                timestamp = time;
+            } else {
+                timestamp = null;
+            }
+        }
+        return timestamp;
+    }
+
+    /**
+     * Group all JsonArrayEntry by time-stamp
+     * 
+     * @return a map (relativeTimestamp => collection of JsonArrayEntry)
+     */
+    private static 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) {
+                // comparator which
+                // - supports null (time null means 0 if there is a base time)
+                // - reverses natural order (most recent value in first)
+                return Long.compare(o2 == null ? 0 : o2, o1 == null ? 0 : o1);
+            }
+        });
+
+        for (JsonArrayEntry e : jsonObject.getResourceList()) {
+            // Get time for this entry
+            Long time = e.getTime();
+
+            // Get jsonArray for this time-stamp
+            Collection<JsonArrayEntry> jsonArray = result.get(time);
+            if (jsonArray == null) {
+                jsonArray = new ArrayList<JsonArrayEntry>();
+                result.put(time, jsonArray);
+            }
+
+            // Add it to the list
+            jsonArray.add(e);
+        }
+
+        return result;
+    }
+
+    /**
+     * Group all JsonArrayEntry by instanceId
+     * 
+     * @param requestPath
+     * @param baseName
+     * @param jsonEntries
+     * 
+     * @return a map (instanceId => collection of JsonArrayEntry)
+     */
+    private static Map<Integer, Collection<JsonArrayEntry>> groupJsonEntryByInstanceId(
+            Collection<JsonArrayEntry> jsonEntries, LwM2mPath baseName) throws InvalidValueException {
+        Map<Integer, Collection<JsonArrayEntry>> result = new HashMap<>();
+
+        for (JsonArrayEntry e : jsonEntries) {
+            // Build resource path
+            LwM2mPath nodePath = baseName.append(e.getName());
+
+            // Validate path
+            if (!nodePath.isResourceInstance() && !nodePath.isResource()) {
+                throw new InvalidValueException(
+                        "Invalid path for resource, it should be a resource or a resource instance path", nodePath);
+            }
+
+            // Get jsonArray for this instance
+            Collection<JsonArrayEntry> jsonArray = result.get(nodePath.getObjectInstanceId());
+            if (jsonArray == null) {
+                jsonArray = new ArrayList<>();
+                result.put(nodePath.getObjectInstanceId(), jsonArray);
+            }
+
+            // Add it to the list
+            jsonArray.add(e);
         }
+
+        return result;
     }
 
     private static LwM2mPath extractAndValidateBaseName(JsonRootObject jsonObject, LwM2mPath requestPath)
@@ -114,23 +272,18 @@ public class LwM2mNodeJsonDecoder {
 
     }
 
-    private static Map<Integer, LwM2mResource> parseJsonPayLoadLwM2mResources(JsonRootObject jsonObject,
-            LwM2mPath requestPath, LwM2mPath baseName, LwM2mModel model) throws InvalidValueException {
+    private static Map<Integer, LwM2mResource> extractLwM2mResources(Collection<JsonArrayEntry> jsonArrayEntries,
+            LwM2mPath baseName, LwM2mModel model) throws InvalidValueException {
+        if (jsonArrayEntries == null)
+            return Collections.emptyMap();
 
         // Extract LWM2M resources from JSON resource list
         Map<Integer, LwM2mResource> lwM2mResourceMap = new HashMap<>();
         Map<LwM2mPath, Map<Integer, JsonArrayEntry>> multiResourceMap = new HashMap<>();
-        for (int i = 0; i < jsonObject.getResourceList().size(); i++) {
-            JsonArrayEntry resourceElt = jsonObject.getResourceList().get(i);
+        for (JsonArrayEntry resourceElt : jsonArrayEntries) {
 
             // Build resource path
-            LwM2mPath nodePath;
-            if (baseName != null) {
-                nodePath = baseName.append(resourceElt.getName());
-            } else {
-                // we don't have a baseName so use request path
-                nodePath = requestPath.append(resourceElt.getName());
-            }
+            LwM2mPath nodePath = baseName.append(resourceElt.getName());
 
             // handle LWM2M resources
             if (nodePath.isResourceInstance()) {
@@ -223,4 +376,4 @@ public class LwM2mNodeJsonDecoder {
             return rscDesc.type;
         }
     }
-}
+}
\ No newline at end of file
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 d8384747..d95124ea 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
@@ -17,6 +17,7 @@ package org.eclipse.leshan.core.node.codec.json;
 
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 import java.util.Map.Entry;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
@@ -28,6 +29,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.node.codec.Lwm2mNodeEncoderUtil;
 import org.eclipse.leshan.json.JsonArrayEntry;
 import org.eclipse.leshan.json.JsonRootObject;
@@ -49,73 +51,130 @@ public class LwM2mNodeJsonEncoder {
         InternalEncoder internalEncoder = new InternalEncoder();
         internalEncoder.objectId = path.getObjectId();
         internalEncoder.model = model;
-        internalEncoder.path = path;
+        internalEncoder.requestPath = path;
         node.accept(internalEncoder);
-        return internalEncoder.encoded;
+        JsonRootObject jsonObject = new JsonRootObject(internalEncoder.resourceList);
+        return LwM2mJson.toJsonLwM2m(jsonObject).getBytes();
+    }
+
+    public static byte[] encodeTimestampedData(List<TimestampedLwM2mNode> timestampedNodes, LwM2mPath path,
+            LwM2mModel model) {
+        Validate.notNull(timestampedNodes);
+        Validate.notNull(path);
+        Validate.notNull(model);
+
+        InternalEncoder internalEncoder = new InternalEncoder();
+        ArrayList<JsonArrayEntry> entries = new ArrayList<>();
+        for (TimestampedLwM2mNode timestampedLwM2mNode : timestampedNodes) {
+            internalEncoder.objectId = path.getObjectId();
+            internalEncoder.model = model;
+            internalEncoder.requestPath = path;
+            internalEncoder.resourceList = null;
+            internalEncoder.timestamp = timestampedLwM2mNode.getTimestamp();
+            timestampedLwM2mNode.getNode().accept(internalEncoder);
+            entries.addAll(internalEncoder.resourceList);
+        }
+        JsonRootObject jsonObject = new JsonRootObject(entries);
+
+        return LwM2mJson.toJsonLwM2m(jsonObject).getBytes();
     }
 
     private static class InternalEncoder implements LwM2mNodeVisitor {
         // visitor inputs
         private int objectId;
         private LwM2mModel model;
-        private LwM2mPath path;
+        private LwM2mPath requestPath;
+        private Long timestamp;
 
         // visitor output
-        private byte[] encoded = null;
+        private ArrayList<JsonArrayEntry> resourceList = null;
 
         @Override
         public void visit(LwM2mObject object) {
-            // TODO needed to encode multiple instances
-            // How to deal with ObjLink
-            throw new UnsupportedOperationException("Object JSON encoding not supported");
+            LOG.trace("Encoding Object {} into JSON", object);
+            // Validate request path
+            if (!requestPath.isObject()) {
+                throw new IllegalArgumentException("Invalid request path for JSON object encoding");
+            }
+
+            // Create resources
+            resourceList = new ArrayList<>();
+            for (LwM2mObjectInstance instance : object.getInstances().values()) {
+                for (LwM2mResource resource : instance.getResources().values()) {
+                    String prefixPath = Integer.toString(instance.getId()) + "/" + Integer.toString(resource.getId());
+                    resourceList.addAll(lwM2mResourceToJsonArrayEntry(prefixPath, timestamp, resource));
+                }
+            }
         }
 
         @Override
         public void visit(LwM2mObjectInstance instance) {
             LOG.trace("Encoding object instance {} into JSON", instance);
-            JsonRootObject jsonObject = null;
-            ArrayList<JsonArrayEntry> resourceList = new ArrayList<>();
-            for (Entry<Integer, LwM2mResource> resource : instance.getResources().entrySet()) {
-                LwM2mResource res = resource.getValue();
-                resourceList.addAll(lwM2mResourceToJsonArrayEntry(res));
-            }
-            jsonObject = new JsonRootObject(resourceList);
-            if (path.isObject()) {
-                jsonObject.setBaseName(new LwM2mPath(path.getObjectId(), instance.getId()).toString() + "/");
+            resourceList = new ArrayList<>();
+            for (LwM2mResource resource : instance.getResources().values()) {
+                // Validate request path & compute resource path
+                String prefixPath = null;
+                if (requestPath.isObject()) {
+                    prefixPath = instance.getId() + "/" + resource.getId();
+                } else if (requestPath.isObjectInstance()) {
+                    prefixPath = Integer.toString(resource.getId());
+                } else {
+                    throw new IllegalArgumentException("Invalid request path for JSON instance encoding");
+                }
+                // Create resources
+                resourceList.addAll(lwM2mResourceToJsonArrayEntry(prefixPath, timestamp, resource));
             }
-            String json = LwM2mJson.toJsonLwM2m(jsonObject);
-            encoded = json.getBytes();
         }
 
         @Override
         public void visit(LwM2mResource resource) {
             LOG.trace("Encoding resource {} into JSON", resource);
-            JsonRootObject jsonObject = null;
-            jsonObject = new JsonRootObject(lwM2mResourceToJsonArrayEntry(resource));
-            jsonObject.setBaseName(new LwM2mPath(path.getObjectId(), path.getObjectInstanceId()).toString() + "/");
-            String json = LwM2mJson.toJsonLwM2m(jsonObject);
-            encoded = json.getBytes();
+            if (!requestPath.isResource()) {
+                throw new IllegalArgumentException("Invalid request path for JSON resource encoding");
+            }
+
+            resourceList = lwM2mResourceToJsonArrayEntry("", timestamp, resource);
         }
 
-        private ArrayList<JsonArrayEntry> lwM2mResourceToJsonArrayEntry(LwM2mResource resource) {
+        private ArrayList<JsonArrayEntry> lwM2mResourceToJsonArrayEntry(String resourcePath, Long timestamp,
+                LwM2mResource resource) {
+            // get type for this resource
             ResourceModel rSpec = model.getResourceModel(objectId, resource.getId());
             Type expectedType = rSpec != null ? rSpec.type : resource.getType();
             ArrayList<JsonArrayEntry> resourcesList = new ArrayList<>();
+
+            // create JSON resource element
             if (resource.isMultiInstances()) {
                 for (Entry<Integer, ?> entry : resource.getValues().entrySet()) {
+                    // Create resource element
                     JsonArrayEntry jsonResourceElt = new JsonArrayEntry();
-                    jsonResourceElt.setName(resource.getId() + "/" + entry.getKey());
+                    if (resourcePath == null || resourcePath.isEmpty()) {
+                        jsonResourceElt.setName(Integer.toString(entry.getKey()));
+                    } else {
+                        jsonResourceElt.setName(resourcePath + "/" + entry.getKey());
+                    }
+                    jsonResourceElt.setTime(timestamp);
+
+                    // Convert value using expected type
                     Object convertedValue = Lwm2mNodeEncoderUtil.convertValue(entry.getValue(), resource.getType(),
                             expectedType);
                     this.setResourceValue(convertedValue, expectedType, jsonResourceElt);
+
+                    // Add it to the List
                     resourcesList.add(jsonResourceElt);
                 }
             } else {
+                // Create resource element
                 JsonArrayEntry jsonResourceElt = new JsonArrayEntry();
-                jsonResourceElt.setName(new StringBuffer().append(resource.getId()).toString());
+                jsonResourceElt.setName(resourcePath);
+                jsonResourceElt.setTime(timestamp);
+
+                // Convert value using expected type
                 this.setResourceValue(
                         Lwm2mNodeEncoderUtil.convertValue(resource.getValue(), resource.getType(), expectedType),
                         expectedType, jsonResourceElt);
+
+                // Add it to the List
                 resourcesList.add(jsonResourceElt);
             }
             return resourcesList;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java
index 87373064..c1d03c35 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ContentFormat.java
@@ -18,21 +18,41 @@ package org.eclipse.leshan.core.request;
 /**
  * Data format defined by the LWM2M specification
  */
-public enum ContentFormat {
+public class ContentFormat {
+    public static final int TLV_CODE = 1542;
+    public static final int JSON_CODE = 1543;
+    public static final int TEXT_CODE = 0;
+    public static final int OPAQUE_CODE = 42;
+    public static final int LINK_CODE = 40;
 
-    // TODO: update TLV and JSON media type codes once they have been assigned by IANA
-    LINK("application/link-format", 40), TEXT("text/plain", 0), TLV(
-            "application/vnd.oma.lwm2m+tlv", 1542), JSON("application/vnd.oma.lwm2m+json", 1543), OPAQUE(
-            "application/octet-stream", 42);
+    public static final ContentFormat TLV = new ContentFormat("TLV", "application/vnd.oma.lwm2m+tlv", TLV_CODE);
+    public static final ContentFormat JSON = new ContentFormat("JSON", "application/vnd.oma.lwm2m+json", JSON_CODE);
+    public static final ContentFormat TEXT = new ContentFormat("TEXT", "text/plain", TEXT_CODE);
+    public static final ContentFormat OPAQUE = new ContentFormat("OPAQUE", "application/octet-stream", OPAQUE_CODE);
+    public static final ContentFormat LINK = new ContentFormat("LINK", "application/link-format", LINK_CODE);
 
+    private static final ContentFormat knownContentFormat[] = new ContentFormat[] { TLV, JSON, TEXT, OPAQUE, LINK };
+
+    private final String name;
     private final String mediaType;
     private final int code;
 
-    private ContentFormat(String mediaType, int code) {
+    public ContentFormat(String name, String mediaType, int code) {
+        this.name = name;
         this.mediaType = mediaType;
         this.code = code;
     }
 
+    public ContentFormat(int code) {
+        this.name = "UNKNOWN";
+        this.mediaType = "unknown/unknown";
+        this.code = code;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
     public String getMediaType() {
         return this.mediaType;
     }
@@ -45,7 +65,7 @@ public enum ContentFormat {
      * Find the {@link ContentFormat} for the given media type (<code>null</code> if not found)
      */
     public static ContentFormat fromMediaType(String mediaType) {
-        for (ContentFormat t : ContentFormat.values()) {
+        for (ContentFormat t : knownContentFormat) {
             if (t.getMediaType().equals(mediaType)) {
                 return t;
             }
@@ -59,11 +79,25 @@ public enum ContentFormat {
      * @return the media type or <code>null</code> if the given code is unknown
      */
     public static ContentFormat fromCode(int code) {
-        for (ContentFormat t : ContentFormat.values()) {
+        for (ContentFormat t : knownContentFormat) {
             if (t.getCode() == code) {
                 return t;
             }
         }
+        return new ContentFormat(code);
+    }
+
+    /**
+     * Finds the {@link ContentFormat} by name.
+     *
+     * @return the media type or <code>null</code> if the given code is unknown
+     */
+    public static ContentFormat fromName(String name) {
+        for (ContentFormat t : knownContentFormat) {
+            if (t.getName() == name) {
+                return t;
+            }
+        }
         return null;
     }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/json/JsonArrayEntry.java b/leshan-core/src/main/java/org/eclipse/leshan/json/JsonArrayEntry.java
index d7b39c0e..e5764018 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/json/JsonArrayEntry.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/json/JsonArrayEntry.java
@@ -40,7 +40,7 @@ public class JsonArrayEntry {
     private String stringValue;
 
     @SerializedName("t")
-    private Integer time;
+    private Long time;
 
     public ResourceModel.Type getType() {
         if (booleanValue != null) {
@@ -67,11 +67,11 @@ public class JsonArrayEntry {
         this.name = name;
     }
 
-    public Integer getTime() {
+    public Long getTime() {
         return time;
     }
 
-    public void setTime(Integer time) {
+    public void setTime(Long time) {
         this.time = time;
     }
 
@@ -117,7 +117,7 @@ public class JsonArrayEntry {
         result = prime * result + ((name == null) ? 0 : name.hashCode());
         result = prime * result + ((objectLinkValue == null) ? 0 : objectLinkValue.hashCode());
         result = prime * result + ((stringValue == null) ? 0 : stringValue.hashCode());
-        result = prime * result + time;
+        result = prime * result + ((time == null) ? 0 : time.hashCode());
         return result;
     }
 
@@ -162,9 +162,9 @@ public class JsonArrayEntry {
 
     @Override
     public String toString() {
-        return String
-                .format("JsonArrayElement [name=%s, floatValue=%s, booleanValue=%s, objectLinkValue=%s, stringValue=%s, time=%s]",
-                        name, floatValue, booleanValue, objectLinkValue, stringValue, time);
+        return String.format(
+                "JsonArrayElement [name=%s, floatValue=%s, booleanValue=%s, objectLinkValue=%s, stringValue=%s, time=%s]",
+                name, floatValue, booleanValue, objectLinkValue, stringValue, time);
     }
 
 }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java
index c6af011b..cc9bf43c 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/LeshanServerBuilder.java
@@ -19,6 +19,10 @@ import java.net.InetAddress;
 import java.net.InetSocketAddress;
 
 import org.eclipse.californium.core.observe.InMemoryObservationStore;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.server.LwM2mServer;
 import org.eclipse.leshan.server.californium.impl.CaliforniumObservationRegistryImpl;
 import org.eclipse.leshan.server.californium.impl.LeshanServer;
@@ -49,6 +53,10 @@ public class LeshanServerBuilder {
     private InetSocketAddress localAddress;
     private InetSocketAddress localSecureAddress;
 
+    private LwM2mNodeEncoder encoder;
+
+    private LwM2mNodeDecoder decoder;
+
     public LeshanServerBuilder setLocalAddress(String hostname, int port) {
         if (hostname == null) {
             this.localAddress = new InetSocketAddress(port);
@@ -97,6 +105,16 @@ public class LeshanServerBuilder {
         return this;
     }
 
+    public LeshanServerBuilder setEncoder(LwM2mNodeEncoder encoder) {
+        this.encoder = encoder;
+        return this;
+    }
+
+    public LeshanServerBuilder setDecoder(LwM2mNodeDecoder decoder) {
+        this.decoder = decoder;
+        return this;
+    }
+
     public LeshanServer build() {
         if (localAddress == null)
             localAddress = new InetSocketAddress((InetAddress) null, PORT);
@@ -108,11 +126,16 @@ public class LeshanServerBuilder {
             securityRegistry = new SecurityRegistryImpl();
         if (modelProvider == null)
             modelProvider = new StandardModelProvider();
+        if (encoder == null)
+            encoder = new DefaultLwM2mNodeEncoder();
+        if (decoder == null)
+            decoder = new DefaultLwM2mNodeDecoder();
+
         if (observationRegistry == null)
             observationRegistry = new CaliforniumObservationRegistryImpl(new InMemoryObservationStore(), clientRegistry,
-                    modelProvider);
+                    modelProvider, decoder);
 
         return new LeshanServer(localAddress, localSecureAddress, clientRegistry, securityRegistry, observationRegistry,
-                modelProvider);
+                modelProvider, encoder, decoder);
     }
 }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumLwM2mBootstrapRequestSender.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumLwM2mBootstrapRequestSender.java
index 8031a363..c7211b62 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumLwM2mBootstrapRequestSender.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumLwM2mBootstrapRequestSender.java
@@ -21,6 +21,10 @@ import org.eclipse.californium.core.coap.Request;
 import org.eclipse.californium.core.coap.Response;
 import org.eclipse.californium.core.network.Endpoint;
 import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.DownlinkRequest;
 import org.eclipse.leshan.core.response.ErrorCallback;
 import org.eclipse.leshan.core.response.LwM2mResponse;
@@ -36,6 +40,8 @@ public class CaliforniumLwM2mBootstrapRequestSender implements LwM2mBootstrapReq
     private final Endpoint nonSecureEndpoint;
     private final Endpoint secureEndpoint;
     private final LwM2mModel model;
+    private final LwM2mNodeDecoder decoder = new DefaultLwM2mNodeDecoder();
+    private final LwM2mNodeEncoder encoder = new DefaultLwM2mNodeEncoder();
 
     public CaliforniumLwM2mBootstrapRequestSender(final Endpoint secureEndpoint, final Endpoint nonSecureEndpoint,
             final LwM2mModel model) {
@@ -48,7 +54,7 @@ public class CaliforniumLwM2mBootstrapRequestSender implements LwM2mBootstrapReq
     public <T extends LwM2mResponse> T send(final String endpointName, final InetSocketAddress clientAddress,
             final boolean secure, final DownlinkRequest<T> request, Long timeout) throws InterruptedException {
         // Create the CoAP request from LwM2m request
-        final CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(clientAddress, model);
+        final CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(clientAddress, model, encoder);
         request.accept(coapClientRequestBuilder);
 
         final Request coapRequest = coapClientRequestBuilder.getRequest();
@@ -64,7 +70,7 @@ public class CaliforniumLwM2mBootstrapRequestSender implements LwM2mBootstrapReq
                                 .build();
                 // Build LwM2m response
                 final LwM2mResponseBuilder<T> lwm2mResponseBuilder = new LwM2mResponseBuilder<T>(coapRequest,
-                        coapResponse, client, model, null);
+                        coapResponse, client, model, null, decoder);
                 request.accept(lwm2mResponseBuilder);
                 return lwm2mResponseBuilder.getResponse();
             }
@@ -86,7 +92,7 @@ public class CaliforniumLwM2mBootstrapRequestSender implements LwM2mBootstrapReq
             final boolean secure, final DownlinkRequest<T> request, final ResponseCallback<T> responseCallback,
             final ErrorCallback errorCallback) {
         // Create the CoAP request from LwM2m request
-        final CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(clientAddress, model);
+        final CoapRequestBuilder coapClientRequestBuilder = new CoapRequestBuilder(clientAddress, model, encoder);
         request.accept(coapClientRequestBuilder);
         final Request coapRequest = coapClientRequestBuilder.getRequest();
 
@@ -103,7 +109,7 @@ public class CaliforniumLwM2mBootstrapRequestSender implements LwM2mBootstrapReq
 
                 // Build LwM2m response
                 final LwM2mResponseBuilder<T> lwm2mResponseBuilder = new LwM2mResponseBuilder<T>(coapRequest,
-                        coapResponse, client, model, null);
+                        coapResponse, client, model, null, decoder);
                 request.accept(lwm2mResponseBuilder);
                 return lwm2mResponseBuilder.getResponse();
             }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumLwM2mRequestSender.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumLwM2mRequestSender.java
index c7c0c267..8d7e1c4a 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumLwM2mRequestSender.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumLwM2mRequestSender.java
@@ -30,6 +30,8 @@ import org.eclipse.californium.core.coap.Request;
 import org.eclipse.californium.core.coap.Response;
 import org.eclipse.californium.core.network.Endpoint;
 import org.eclipse.leshan.core.model.LwM2mModel;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.DownlinkRequest;
 import org.eclipse.leshan.core.response.ErrorCallback;
 import org.eclipse.leshan.core.response.LwM2mResponse;
@@ -48,6 +50,8 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender {
     private final Set<Endpoint> endpoints;
     private final ObservationRegistry observationRegistry;
     private final LwM2mModelProvider modelProvider;
+    private final LwM2mNodeDecoder decoder;
+    private final LwM2mNodeEncoder encoder;
     // A map which contains all pending CoAP requests
     // This is mainly used to cancel request and avoid retransmission on de-registration
     private final ConcurrentNavigableMap<String/* registrationId#requestId */, Request /* pending coap Request */> pendingRequests = new ConcurrentSkipListMap<>();
@@ -61,13 +65,15 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender {
      * @param modelProvider provides the supported objects definitions
      */
     public CaliforniumLwM2mRequestSender(final Set<Endpoint> endpoints, final ObservationRegistry observationRegistry,
-            LwM2mModelProvider modelProvider) {
+            LwM2mModelProvider modelProvider, LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder) {
         Validate.notNull(endpoints);
         Validate.notNull(observationRegistry);
         Validate.notNull(modelProvider);
         this.observationRegistry = observationRegistry;
         this.endpoints = endpoints;
         this.modelProvider = modelProvider;
+        this.encoder = encoder;
+        this.decoder = decoder;
     }
 
     @Override
@@ -80,7 +86,7 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender {
         // Create the CoAP request from LwM2m request
         final CoapRequestBuilder coapRequestBuilder = new CoapRequestBuilder(
                 new InetSocketAddress(destination.getAddress(), destination.getPort()), destination.getRootPath(),
-                model);
+                model, encoder);
         request.accept(coapRequestBuilder);
         final Request coapRequest = coapRequestBuilder.getRequest();
 
@@ -90,7 +96,7 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender {
             public T buildResponse(final Response coapResponse) {
                 // Build LwM2m response
                 final LwM2mResponseBuilder<T> lwm2mResponseBuilder = new LwM2mResponseBuilder<T>(coapRequest,
-                        coapResponse, destination, model, observationRegistry);
+                        coapResponse, destination, model, observationRegistry, decoder);
                 request.accept(lwm2mResponseBuilder);
                 return lwm2mResponseBuilder.getResponse();
             }
@@ -117,7 +123,7 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender {
         // Create the CoAP request from LwM2m request
         final CoapRequestBuilder coapRequestBuilder = new CoapRequestBuilder(
                 new InetSocketAddress(destination.getAddress(), destination.getPort()), destination.getRootPath(),
-                model);
+                model, encoder);
         request.accept(coapRequestBuilder);
         final Request coapRequest = coapRequestBuilder.getRequest();
 
@@ -127,7 +133,7 @@ public class CaliforniumLwM2mRequestSender implements LwM2mRequestSender {
             public T buildResponse(final Response coapResponse) {
                 // Build LwM2m response
                 final LwM2mResponseBuilder<T> lwm2mResponseBuilder = new LwM2mResponseBuilder<T>(coapRequest,
-                        coapResponse, destination, model, observationRegistry);
+                        coapResponse, destination, model, observationRegistry, decoder);
                 request.accept(lwm2mResponseBuilder);
                 return lwm2mResponseBuilder.getResponse();
             }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationRegistryImpl.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationRegistryImpl.java
index 4170c0d4..205d4041 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationRegistryImpl.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationRegistryImpl.java
@@ -31,8 +31,8 @@ import org.eclipse.californium.core.network.Exchange.KeyToken;
 import org.eclipse.californium.core.observe.NotificationListener;
 import org.eclipse.californium.core.observe.ObservationStore;
 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;
 import org.eclipse.leshan.core.node.codec.InvalidValueException;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
 import org.eclipse.leshan.core.observation.Observation;
@@ -54,6 +54,7 @@ public class CaliforniumObservationRegistryImpl
     private final ObservationStore observationStore;
     private final ClientRegistry clientRegistry;
     private final LwM2mModelProvider modelProvider;
+    private final LwM2mNodeDecoder decoder;
     private Endpoint secureEndpoint;
     private Endpoint nonSecureEndpoint;
 
@@ -61,10 +62,11 @@ public class CaliforniumObservationRegistryImpl
     private final Map<KeyToken, Observation> observations = new ConcurrentHashMap<KeyToken, Observation>();
 
     public CaliforniumObservationRegistryImpl(ObservationStore store, ClientRegistry clientRegistry,
-            LwM2mModelProvider modelProvider) {
+            LwM2mModelProvider modelProvider, LwM2mNodeDecoder decoder) {
         this.observationStore = store;
         this.modelProvider = modelProvider;
         this.clientRegistry = clientRegistry;
+        this.decoder = decoder;
     }
 
     @Override
@@ -179,6 +181,7 @@ public class CaliforniumObservationRegistryImpl
 
     // ********** NotificationListener interface **********//
 
+    // TODO duplicate code from org.eclipse.leshan.server.demo.cluster.RedisObservationRegistry
     @Override
     public void onNotification(Request coapRequest, Response coapResponse) {
         if (listeners.isEmpty())
@@ -202,13 +205,19 @@ public class CaliforniumObservationRegistryImpl
                 LwM2mModel model = modelProvider.getObjectModel(client);
 
                 // decode response
-                LwM2mNode content = LwM2mNodeDecoder.decode(coapResponse.getPayload(),
+                List<TimestampedLwM2mNode> content = decoder.decodeTimestampedData(coapResponse.getPayload(),
                         ContentFormat.fromCode(coapResponse.getOptions().getContentFormat()), observation.getPath(),
                         model);
 
                 // notify all listeners
                 for (ObservationRegistryListener listener : listeners) {
-                    listener.newValue(observation, content);
+                    if (content.isEmpty()) {
+                        listener.newValue(observation, null, content);
+                    } else if (content.size() == 1 && content.get(0).isTimespamped()) {
+                        listener.newValue(observation, content.get(0).getNode(), null);
+                    } else {
+                        listener.newValue(observation, content.get(0).getNode(), content);
+                    }
                 }
             } catch (InvalidValueException e) {
                 String msg = String.format("[%s] ([%s])", e.getMessage(), e.getPath().toString());
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilder.java
index 3a8d1ca1..daeede35 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilder.java
@@ -44,17 +44,18 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor {
     private final InetSocketAddress destination;
     private final String rootPath;
     private final LwM2mModel model;
+    private final LwM2mNodeEncoder encoder;
 
-    public CoapRequestBuilder(InetSocketAddress destination, LwM2mModel model) {
-        this.destination = destination;
-        this.model = model;
-        this.rootPath = null;
+    public CoapRequestBuilder(InetSocketAddress destination, LwM2mModel model, LwM2mNodeEncoder encoder) {
+        this(destination, null, model, encoder);
     }
 
-    public CoapRequestBuilder(InetSocketAddress destination, String rootpath, LwM2mModel model) {
+    public CoapRequestBuilder(InetSocketAddress destination, String rootpath, LwM2mModel model,
+            LwM2mNodeEncoder encoder) {
         this.destination = destination;
         this.model = model;
         this.rootPath = rootpath;
+        this.encoder = encoder;
     }
 
     @Override
@@ -77,7 +78,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor {
         coapRequest = request.isReplaceRequest() ? Request.newPut() : Request.newPost();
         ContentFormat format = request.getContentFormat();
         coapRequest.getOptions().setContentFormat(format.getCode());
-        coapRequest.setPayload(LwM2mNodeEncoder.encode(request.getNode(), format, request.getPath(), model));
+        coapRequest.setPayload(encoder.encode(request.getNode(), format, request.getPath(), model));
         setTarget(coapRequest, request.getPath());
     }
 
@@ -103,7 +104,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor {
         coapRequest.getOptions().setContentFormat(request.getContentFormat().getCode());
         // if no instance id, the client will assign it.
         int instanceId = request.getInstanceId() != null ? request.getInstanceId() : LwM2mObjectInstance.UNDEFINED;
-        coapRequest.setPayload(LwM2mNodeEncoder.encode(new LwM2mObjectInstance(instanceId, request.getResources()),
+        coapRequest.setPayload(encoder.encode(new LwM2mObjectInstance(instanceId, request.getResources()),
                 request.getContentFormat(), request.getPath(), model));
         setTarget(coapRequest, request.getPath());
     }
@@ -129,7 +130,7 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor {
         coapRequest.setConfirmable(true);
         ContentFormat format = request.getContentFormat();
         coapRequest.getOptions().setContentFormat(format.getCode());
-        coapRequest.setPayload(LwM2mNodeEncoder.encode(request.getNode(), format, request.getPath(), model));
+        coapRequest.setPayload(encoder.encode(request.getNode(), format, request.getPath(), model));
         setTarget(coapRequest, request.getPath());
     }
 
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java
index 9be4dfdd..0ebee82f 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LeshanServer.java
@@ -35,6 +35,8 @@ import org.eclipse.californium.core.server.resources.Resource;
 import org.eclipse.californium.scandium.DTLSConnector;
 import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
 import org.eclipse.californium.scandium.config.DtlsConnectorConfig.Builder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.observation.Observation;
 import org.eclipse.leshan.core.request.DownlinkRequest;
 import org.eclipse.leshan.core.response.ErrorCallback;
@@ -92,6 +94,10 @@ public class LeshanServer implements LwM2mServer {
 
     private final CoapEndpoint secureEndpoint;
 
+    private final LwM2mNodeEncoder encoder;
+
+    private final LwM2mNodeDecoder decoder;
+
     /**
      * Initialize a server which will bind to the specified address and port.
      *
@@ -101,22 +107,29 @@ public class LeshanServer implements LwM2mServer {
      * @param securityRegistry the {@link SecurityInfo} registry.
      * @param observationRegistry the {@link Observation} registry.
      * @param modelProvider provides the objects description for each client.
+     * @param decoder
+     * @param encoder
      */
     public LeshanServer(InetSocketAddress localAddress, InetSocketAddress localSecureAddress,
             final ClientRegistry clientRegistry, final SecurityRegistry securityRegistry,
-            final CaliforniumObservationRegistry observationRegistry, final LwM2mModelProvider modelProvider) {
+            final CaliforniumObservationRegistry observationRegistry, final LwM2mModelProvider modelProvider,
+            LwM2mNodeEncoder encoder, LwM2mNodeDecoder decoder) {
         Validate.notNull(localAddress, "IP address cannot be null");
         Validate.notNull(localSecureAddress, "Secure IP address cannot be null");
         Validate.notNull(clientRegistry, "clientRegistry cannot be null");
         Validate.notNull(securityRegistry, "securityRegistry cannot be null");
         Validate.notNull(observationRegistry, "observationRegistry cannot be null");
         Validate.notNull(modelProvider, "modelProvider cannot be null");
+        Validate.notNull(encoder, "encoder cannot be null");
+        Validate.notNull(decoder, "decoder cannot be null");
 
         // Init registries
         this.clientRegistry = clientRegistry;
         this.securityRegistry = securityRegistry;
         this.observationRegistry = observationRegistry;
         this.modelProvider = modelProvider;
+        this.encoder = encoder;
+        this.decoder = decoder;
 
         // Cancel observations on client unregistering
         this.clientRegistry.addListener(new ClientRegistryListener() {
@@ -185,7 +198,8 @@ public class LeshanServer implements LwM2mServer {
         final Set<Endpoint> endpoints = new HashSet<>();
         endpoints.add(nonSecureEndpoint);
         endpoints.add(secureEndpoint);
-        requestSender = new CaliforniumLwM2mRequestSender(endpoints, this.observationRegistry, modelProvider);
+        requestSender = new CaliforniumLwM2mRequestSender(endpoints, this.observationRegistry, modelProvider, encoder,
+                decoder);
     }
 
     @Override
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LwM2mResponseBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LwM2mResponseBuilder.java
index 0e3f681a..f5100ea5 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LwM2mResponseBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/impl/LwM2mResponseBuilder.java
@@ -69,6 +69,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
     private final ObservationRegistry observationRegistry;
     private final Client client;
     private final LwM2mModel model;
+    private final LwM2mNodeDecoder decoder;
 
     // TODO leshan-code-cf: this code should be factorize in a leshan-core-cf project.
     // duplicate from org.eclipse.leshan.client.californium.impl.LwM2mClientResponseBuilder<T>
@@ -105,12 +106,13 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
     }
 
     public LwM2mResponseBuilder(final Request coapRequest, final Response coapResponse, final Client client,
-            final LwM2mModel model, final ObservationRegistry observationRegistry) {
+            final LwM2mModel model, final ObservationRegistry observationRegistry, final LwM2mNodeDecoder decoder) {
         this.coapRequest = coapRequest;
         this.coapResponse = coapResponse;
         this.observationRegistry = observationRegistry;
         this.client = client;
         this.model = model;
+        this.decoder = decoder;
     }
 
     @Override
@@ -347,7 +349,7 @@ public class LwM2mResponseBuilder<T extends LwM2mResponse> implements DownlinkRe
     private LwM2mNode decodeCoapResponse(final LwM2mPath path, final Response coapResponse) {
         LwM2mNode content;
         try {
-            content = LwM2mNodeDecoder.decode(coapResponse.getPayload(),
+            content = decoder.decode(coapResponse.getPayload(),
                     ContentFormat.fromCode(coapResponse.getOptions().getContentFormat()), path, model);
         } catch (final InvalidValueException e) {
             final String msg = String.format("[%s] (%s:%s)", e.getMessage(), e.getPath().toString(),
diff --git a/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisObservationRegistry.java b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisObservationRegistry.java
index 9888c88d..5b04bb7f 100644
--- a/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisObservationRegistry.java
+++ b/leshan-server-cluster/src/main/java/org/eclipse/leshan/server/demo/cluster/RedisObservationRegistry.java
@@ -33,8 +33,8 @@ import org.eclipse.californium.core.observe.NotificationListener;
 import org.eclipse.californium.core.observe.ObservationStore;
 import org.eclipse.californium.elements.CorrelationContext;
 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;
 import org.eclipse.leshan.core.node.codec.InvalidValueException;
 import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
 import org.eclipse.leshan.core.observation.Observation;
@@ -64,6 +64,7 @@ public class RedisObservationRegistry
     private final ObservationStore observationStore;
     private final ClientRegistry clientRegistry;
     private final LwM2mModelProvider modelProvider;
+    private final LwM2mNodeDecoder decoder;
     private Endpoint nonSecureEndpoint;
     private Endpoint secureEndpoint;
 
@@ -80,11 +81,13 @@ public class RedisObservationRegistry
     private static final byte[] PATH = "path".getBytes();
     private static final byte[] REGID = "regid".getBytes();
 
-    public RedisObservationRegistry(Pool<Jedis> pool, ClientRegistry clientRegistry, LwM2mModelProvider modelProvider) {
+    public RedisObservationRegistry(Pool<Jedis> pool, ClientRegistry clientRegistry, LwM2mModelProvider modelProvider,
+            LwM2mNodeDecoder decoder) {
         this.pool = pool;
         this.modelProvider = modelProvider;
         this.clientRegistry = clientRegistry;
         this.observationStore = new RedisObservationStore(pool);
+        this.decoder = decoder;
     }
 
     @Override
@@ -268,6 +271,7 @@ public class RedisObservationRegistry
 
     // ********** NotificationListener interface **********
 
+    // TODO duplicate code from org.eclipse.leshan.server.californium.impl.CaliforniumObservationRegistryImpl
     @Override
     public void onNotification(Request coapRequest, Response coapResponse) {
         if (listeners.isEmpty())
@@ -291,13 +295,19 @@ public class RedisObservationRegistry
                 LwM2mModel model = modelProvider.getObjectModel(client);
 
                 // decode response
-                LwM2mNode content = LwM2mNodeDecoder.decode(coapResponse.getPayload(),
+                List<TimestampedLwM2mNode> content = decoder.decodeTimestampedData(coapResponse.getPayload(),
                         ContentFormat.fromCode(coapResponse.getOptions().getContentFormat()), observation.getPath(),
                         model);
 
                 // notify all listeners
                 for (ObservationRegistryListener listener : listeners) {
-                    listener.newValue(observation, content);
+                    if (content.isEmpty()) {
+                        listener.newValue(observation, null, content);
+                    } else if (content.size() == 1 && content.get(0).isTimespamped()) {
+                        listener.newValue(observation, content.get(0).getNode(), null);
+                    } else {
+                        listener.newValue(observation, content.get(0).getNode(), content);
+                    }
                 }
             } catch (InvalidValueException e) {
                 String msg = String.format("[%s] ([%s])", e.getMessage(), e.getPath().toString());
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistryListener.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistryListener.java
index 0ab43850..f3bb49e8 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistryListener.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/observation/ObservationRegistryListener.java
@@ -15,7 +15,10 @@
  *******************************************************************************/
 package org.eclipse.leshan.server.observation;
 
+import java.util.List;
+
 import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.observation.Observation;
 
 public interface ObservationRegistryListener {
@@ -23,5 +26,12 @@ public interface ObservationRegistryListener {
 
     void cancelled(Observation observation);
 
-    void newValue(Observation observation, LwM2mNode value);
+    /**
+     * Called on new notification.
+     * 
+     * @param observation the observation for which new data are received
+     * @param mostRecentValue the most recent value
+     * @param timestampedValues the list of time-stamped value (could be null)
+     */
+    void newValue(Observation observation, LwM2mNode mostRecentValue, List<TimestampedLwM2mNode> timestampedValues);
 }
diff --git a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/impl/QueuedRequestSender.java b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/impl/QueuedRequestSender.java
index 0d2d0f3f..c8d587f1 100644
--- a/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/impl/QueuedRequestSender.java
+++ b/leshan-server-core/src/main/java/org/eclipse/leshan/server/queue/impl/QueuedRequestSender.java
@@ -17,12 +17,14 @@
 package org.eclipse.leshan.server.queue.impl;
 
 import java.util.Collection;
+import java.util.List;
 import java.util.concurrent.ConcurrentLinkedQueue;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 import java.util.concurrent.TimeUnit;
 
 import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.observation.Observation;
 import org.eclipse.leshan.core.request.DownlinkRequest;
 import org.eclipse.leshan.core.request.exception.TimeoutException;
@@ -54,8 +56,8 @@ import org.slf4j.LoggerFactory;
 public class QueuedRequestSender implements LwM2mRequestSender, Stoppable {
     private static final Logger LOG = LoggerFactory.getLogger(QueuedRequestSender.class);
     private final LwM2mRequestSender delegateSender;
-    private final ExecutorService processingExecutor = Executors
-            .newCachedThreadPool(new NamedThreadFactory("leshan-qmode-processingExecutor-%d"));
+    private final ExecutorService processingExecutor = Executors.newCachedThreadPool(new NamedThreadFactory(
+            "leshan-qmode-processingExecutor-%d"));
     private final MessageStore messageStore;
     private final QueueModeClientRegistryListener queueModeClientRegistryListener;
     private final QueueModeObservationRegistryListener queueModeObservationRegistryListener;
@@ -196,16 +198,16 @@ public class QueuedRequestSender implements LwM2mRequestSender, Stoppable {
     private void processResponse(String clientEndpoint, String requestTicket, LwM2mResponse response) {
         LOG.debug("Received Response -> {}", requestTicket);
         messageStore.deleteFirst(clientEndpoint);
-        processingExecutor
-                .execute(new ResponseProcessingTask(clientEndpoint, requestTicket, responseListeners, response));
+        processingExecutor.execute(new ResponseProcessingTask(clientEndpoint, requestTicket, responseListeners,
+                response));
         processingExecutor.execute(newRequestSendingTask(clientEndpoint));
     }
 
     private void processException(String clientEndpoint, String requestTicket, Exception exception) {
         LOG.debug("Received error response {}", requestTicket);
         messageStore.deleteFirst(clientEndpoint);
-        processingExecutor
-                .execute(new ResponseProcessingTask(clientEndpoint, requestTicket, responseListeners, exception));
+        processingExecutor.execute(new ResponseProcessingTask(clientEndpoint, requestTicket, responseListeners,
+                exception));
         processingExecutor.execute(newRequestSendingTask(clientEndpoint));
     }
 
@@ -220,7 +222,7 @@ public class QueuedRequestSender implements LwM2mRequestSender, Stoppable {
 
     private final class QueueModeObservationRegistryListener implements ObservationRegistryListener {
         @Override
-        public void newValue(Observation observation, LwM2mNode value) {
+        public void newValue(Observation observation, LwM2mNode value, List<TimestampedLwM2mNode> timestampedValues) {
             Client client = clientRegistry.findByRegistrationId(observation.getRegistrationId());
             if (client.usesQueueMode() && clientStatusTracker.setClientReachable(client.getEndpoint())
                     && clientStatusTracker.startClientReceiving(client.getEndpoint())) {
@@ -330,8 +332,7 @@ public class QueuedRequestSender implements LwM2mRequestSender, Stoppable {
         private final String endpoint;
         private final String requestTicket;
 
-        private QueuedRequestImpl(String endpoint, DownlinkRequest<LwM2mResponse> downlinkRequest,
-                String requestTicket) {
+        private QueuedRequestImpl(String endpoint, DownlinkRequest<LwM2mResponse> downlinkRequest, String requestTicket) {
             Validate.notNull(endpoint, "endpoint may not be null");
             Validate.notNull(downlinkRequest, "request may not be null");
             this.downlinkRequest = downlinkRequest;
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
index dc4a933b..9fc27f3a 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
@@ -41,6 +41,9 @@ import org.apache.commons.cli.ParseException;
 import org.eclipse.jetty.server.Server;
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.webapp.WebAppContext;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
 import org.eclipse.leshan.server.californium.LeshanServerBuilder;
 import org.eclipse.leshan.server.californium.impl.LeshanServer;
 import org.eclipse.leshan.server.client.ClientRegistry;
@@ -154,8 +157,8 @@ public class LeshanServerDemo {
         try {
             createAndStartServer(webPort, localAddress, localPort, secureLocalAddress, secureLocalPort, redisUrl);
         } catch (BindException e) {
-            System.out.println(String.format("Web port %s is alreay used, you could change it using 'webport' option.",
-                    webPort));
+            System.out.println(
+                    String.format("Web port %s is alreay used, you could change it using 'webport' option.", webPort));
             formatter.printHelp(USAGE, null, options, FOOTER);
         } catch (Exception e) {
             LOG.error("Jetty stopped with unexcepted error ...", e);
@@ -168,6 +171,9 @@ public class LeshanServerDemo {
         LeshanServerBuilder builder = new LeshanServerBuilder();
         builder.setLocalAddress(localAddress, localPort);
         builder.setLocalSecureAddress(secureLocalAddress, secureLocalPort);
+        builder.setEncoder(new DefaultLwM2mNodeEncoder());
+        LwM2mNodeDecoder decoder = new DefaultLwM2mNodeDecoder();
+        builder.setDecoder(decoder);
 
         // connect to redis if needed
         Pool<Jedis> jedis = null;
@@ -181,12 +187,12 @@ public class LeshanServerDemo {
         PublicKey publicKey = null;
         try {
             // Get point values
-            byte[] publicX = Hex.decodeHex("fcc28728c123b155be410fc1c0651da374fc6ebe7f96606e90d927d188894a73"
-                    .toCharArray());
-            byte[] publicY = Hex.decodeHex("d2ffaa73957d76984633fc1cc54d0b763ca0559a9dff9706e9f4557dacc3f52a"
-                    .toCharArray());
-            byte[] privateS = Hex.decodeHex("1dae121ba406802ef07c193c1ee4df91115aabd79c1ed7f4c0ef7ef6a5449400"
-                    .toCharArray());
+            byte[] publicX = Hex
+                    .decodeHex("fcc28728c123b155be410fc1c0651da374fc6ebe7f96606e90d927d188894a73".toCharArray());
+            byte[] publicY = Hex
+                    .decodeHex("d2ffaa73957d76984633fc1cc54d0b763ca0559a9dff9706e9f4557dacc3f52a".toCharArray());
+            byte[] privateS = Hex
+                    .decodeHex("1dae121ba406802ef07c193c1ee4df91115aabd79c1ed7f4c0ef7ef6a5449400".toCharArray());
 
             // Get Elliptic Curve Parameter spec for secp256r1
             AlgorithmParameters algoParameters = AlgorithmParameters.getInstance("EC");
@@ -213,7 +219,8 @@ public class LeshanServerDemo {
                 ClientRegistry clientRegistry = new RedisClientRegistry(jedis);
                 builder.setSecurityRegistry(new RedisSecurityRegistry(jedis, privateKey, publicKey));
                 builder.setClientRegistry(clientRegistry);
-                builder.setObservationRegistry(new RedisObservationRegistry(jedis, clientRegistry, modelProvider));
+                builder.setObservationRegistry(
+                        new RedisObservationRegistry(jedis, clientRegistry, modelProvider, decoder));
             }
         } catch (InvalidKeySpecException | NoSuchAlgorithmException | InvalidParameterSpecException e) {
             LOG.error("Unable to initialize RPK.", e);
@@ -236,8 +243,8 @@ public class LeshanServerDemo {
         ServletHolder eventServletHolder = new ServletHolder(eventServlet);
         root.addServlet(eventServletHolder, "/event/*");
 
-        ServletHolder clientServletHolder = new ServletHolder(new ClientServlet(lwServer, lwServer.getSecureAddress()
-                .getPort()));
+        ServletHolder clientServletHolder = new ServletHolder(
+                new ClientServlet(lwServer, lwServer.getSecureAddress().getPort()));
         root.addServlet(clientServletHolder, "/api/clients/*");
 
         ServletHolder securityServletHolder = new ServletHolder(new SecurityServlet(lwServer.getSecurityRegistry()));
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java
index 29c47131..f4bd9e7d 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/ClientServlet.java
@@ -133,7 +133,7 @@ public class ClientServlet extends HttpServlet {
                 // get content format
                 String contentFormatParam = req.getParameter(FORMAT_PARAM);
                 ContentFormat contentFormat = contentFormatParam != null
-                        ? ContentFormat.valueOf(contentFormatParam.toUpperCase()) : null;
+                        ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null;
 
                 // create & process request
                 ReadRequest request = new ReadRequest(contentFormat, target);
@@ -179,7 +179,7 @@ public class ClientServlet extends HttpServlet {
                 // get content format
                 String contentFormatParam = req.getParameter(FORMAT_PARAM);
                 ContentFormat contentFormat = contentFormatParam != null
-                        ? ContentFormat.valueOf(contentFormatParam.toUpperCase()) : null;
+                        ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null;
 
                 // create & process request
                 LwM2mNode node = extractLwM2mNode(target, req);
@@ -222,7 +222,7 @@ public class ClientServlet extends HttpServlet {
                     // get content format
                     String contentFormatParam = req.getParameter(FORMAT_PARAM);
                     ContentFormat contentFormat = contentFormatParam != null
-                            ? ContentFormat.valueOf(contentFormatParam.toUpperCase()) : null;
+                            ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null;
 
                     // create & process request
                     ObserveRequest request = new ObserveRequest(contentFormat, target);
@@ -286,7 +286,7 @@ public class ClientServlet extends HttpServlet {
                     // get content format
                     String contentFormatParam = req.getParameter(FORMAT_PARAM);
                     ContentFormat contentFormat = contentFormatParam != null
-                            ? ContentFormat.valueOf(contentFormatParam.toUpperCase()) : null;
+                            ? ContentFormat.fromName(contentFormatParam.toUpperCase()) : null;
 
                     // create & process request
                     LwM2mNode node = extractLwM2mNode(target, req);
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/EventServlet.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/EventServlet.java
index d84df1b1..5c0edcfd 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/EventServlet.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/EventServlet.java
@@ -16,14 +16,16 @@
 package org.eclipse.leshan.server.demo.servlet;
 
 import java.io.IOException;
-import java.util.Set;
 import java.util.Collections;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 
 import javax.servlet.http.HttpServletRequest;
 
 import org.eclipse.californium.core.network.Endpoint;
 import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.observation.Observation;
 import org.eclipse.leshan.server.californium.impl.LeshanServer;
 import org.eclipse.leshan.server.client.Client;
@@ -67,7 +69,8 @@ public class EventServlet extends EventSourceServlet {
 
     private final LeshanServer server;
 
-    private Set<LeshanEventSource> eventSources = Collections.newSetFromMap(new ConcurrentHashMap<LeshanEventSource,Boolean>());
+    private Set<LeshanEventSource> eventSources = Collections
+            .newSetFromMap(new ConcurrentHashMap<LeshanEventSource, Boolean>());
 
     private final ClientRegistryListener clientRegistryListener = new ClientRegistryListener() {
 
@@ -97,7 +100,7 @@ public class EventServlet extends EventSourceServlet {
         }
 
         @Override
-        public void newValue(Observation observation, LwM2mNode value) {
+        public void newValue(Observation observation, LwM2mNode value, List<TimestampedLwM2mNode> timestampedValues) {
             if (LOG.isDebugEnabled()) {
                 LOG.debug("Received notification from [{}] containing value [{}]", observation.getPath(),
                         value.toString());


Test patch:
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java
index 1c53f326..2dd49cf4 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java
@@ -21,6 +21,7 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 
 import java.util.Date;
+import java.util.List;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
@@ -30,6 +31,7 @@ import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.tlv.Tlv;
 import org.eclipse.leshan.tlv.Tlv.TlvType;
@@ -44,16 +46,18 @@ import org.junit.Test;
 public class LwM2mNodeDecoderTest {
 
     private static LwM2mModel model;
+    private static LwM2mNodeDecoder decoder;
 
     @BeforeClass
     public static void loadModel() {
         model = new LwM2mModel(ObjectLoader.loadDefault());
+        decoder = new DefaultLwM2mNodeDecoder();
     }
 
     @Test
     public void text_manufacturer_resource() throws InvalidValueException {
         String value = "MyManufacturer";
-        LwM2mSingleResource resource = (LwM2mSingleResource) LwM2mNodeDecoder.decode(value.getBytes(Charsets.UTF_8),
+        LwM2mSingleResource resource = (LwM2mSingleResource) decoder.decode(value.getBytes(Charsets.UTF_8),
                 ContentFormat.TEXT, new LwM2mPath(3, 0, 0), model);
 
         assertEquals(0, resource.getId());
@@ -65,8 +69,8 @@ public class LwM2mNodeDecoderTest {
     @Test
     public void no_model_and_no_content_type_then_fallback_to_text() throws InvalidValueException {
         String value = "MyManufacturer";
-        LwM2mSingleResource resource = (LwM2mSingleResource) LwM2mNodeDecoder.decode(value.getBytes(Charsets.UTF_8),
-                null, new LwM2mPath(666, 0, 0), model);
+        LwM2mSingleResource resource = (LwM2mSingleResource) decoder.decode(value.getBytes(Charsets.UTF_8), null,
+                new LwM2mPath(666, 0, 0), model);
 
         assertEquals(0, resource.getId());
         assertFalse(resource.isMultiInstances());
@@ -76,7 +80,7 @@ public class LwM2mNodeDecoderTest {
 
     @Test
     public void text_battery_resource() throws InvalidValueException {
-        LwM2mSingleResource resource = (LwM2mSingleResource) LwM2mNodeDecoder.decode("100".getBytes(Charsets.UTF_8),
+        LwM2mSingleResource resource = (LwM2mSingleResource) decoder.decode("100".getBytes(Charsets.UTF_8),
                 ContentFormat.TEXT, new LwM2mPath(3, 0, 9), model);
 
         assertEquals(9, resource.getId());
@@ -90,7 +94,7 @@ public class LwM2mNodeDecoderTest {
         String value = "MyManufacturer";
         byte[] content = TlvEncoder.encode(new Tlv[] { new Tlv(TlvType.RESOURCE_VALUE, null, value.getBytes(), 0) })
                 .array();
-        LwM2mSingleResource resource = (LwM2mSingleResource) LwM2mNodeDecoder.decode(content, ContentFormat.TLV,
+        LwM2mSingleResource resource = (LwM2mSingleResource) decoder.decode(content, ContentFormat.TLV,
                 new LwM2mPath(3, 0, 0), model);
 
         assertEquals(0, resource.getId());
@@ -108,7 +112,7 @@ public class LwM2mNodeDecoderTest {
 
     @Test
     public void tlv_device_object_mono_instance() throws Exception {
-        LwM2mObjectInstance oInstance = ((LwM2mObject) LwM2mNodeDecoder.decode(ENCODED_DEVICE, ContentFormat.TLV,
+        LwM2mObjectInstance oInstance = ((LwM2mObject) decoder.decode(ENCODED_DEVICE, ContentFormat.TLV,
                 new LwM2mPath(3), model)).getInstance(0);
         assertDeviceInstance(oInstance);
     }
@@ -142,16 +146,16 @@ public class LwM2mNodeDecoderTest {
     @Test
     public void tlv_device_object_instance0_from_resources_tlv() throws InvalidValueException {
 
-        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) LwM2mNodeDecoder.decode(ENCODED_DEVICE,
-                ContentFormat.TLV, new LwM2mPath(3, 0), model);
+        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) decoder.decode(ENCODED_DEVICE, ContentFormat.TLV,
+                new LwM2mPath(3, 0), model);
         assertDeviceInstance(oInstance);
     }
 
     @Test
     public void tlv_device_object_instance0_from_resources_tlv__instance_expected() throws InvalidValueException {
 
-        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) LwM2mNodeDecoder.decode(ENCODED_DEVICE,
-                ContentFormat.TLV, new LwM2mPath(3), model, LwM2mObjectInstance.class);
+        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) decoder.decode(ENCODED_DEVICE, ContentFormat.TLV,
+                new LwM2mPath(3), model, LwM2mObjectInstance.class);
         assertDeviceInstance(oInstance);
     }
 
@@ -163,7 +167,7 @@ public class LwM2mNodeDecoderTest {
         System.arraycopy(new byte[] { 8, 0, 119 }, 0, instanceTlv, 0, 3);
         System.arraycopy(ENCODED_DEVICE, 0, instanceTlv, 3, ENCODED_DEVICE.length);
 
-        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) LwM2mNodeDecoder.decode(instanceTlv, ContentFormat.TLV,
+        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) decoder.decode(instanceTlv, ContentFormat.TLV,
                 new LwM2mPath(3, 0), model);
         assertDeviceInstance(oInstance);
     }
@@ -172,8 +176,8 @@ public class LwM2mNodeDecoderTest {
     public void tlv_power_source__array_values() throws InvalidValueException {
         byte[] content = new byte[] { 65, 0, 1, 65, 1, 5 };
 
-        LwM2mResource resource = (LwM2mResource) LwM2mNodeDecoder.decode(content, ContentFormat.TLV, new LwM2mPath(3,
-                0, 6), model);
+        LwM2mResource resource = (LwM2mResource) decoder.decode(content, ContentFormat.TLV, new LwM2mPath(3, 0, 6),
+                model);
 
         assertEquals(6, resource.getId());
         assertEquals(2, resource.getValues().size());
@@ -187,8 +191,8 @@ public class LwM2mNodeDecoderTest {
         // is probably not compliant with the spec but it should be supported by the server
         byte[] content = new byte[] { -122, 6, 65, 0, 1, 65, 1, 5 };
 
-        LwM2mResource resource = (LwM2mResource) LwM2mNodeDecoder.decode(content, ContentFormat.TLV, new LwM2mPath(3,
-                0, 6), model);
+        LwM2mResource resource = (LwM2mResource) decoder.decode(content, ContentFormat.TLV, new LwM2mPath(3, 0, 6),
+                model);
 
         assertEquals(6, resource.getId());
         assertEquals(2, resource.getValues().size());
@@ -199,22 +203,21 @@ public class LwM2mNodeDecoderTest {
     @Test(expected = InvalidValueException.class)
     public void tlv_multi_instance_object__missing_instance_tlv() throws InvalidValueException {
 
-        byte[] content = TlvEncoder.encode(
-                new Tlv[] { new Tlv(TlvType.RESOURCE_VALUE, null, "value1".getBytes(), 1),
-                                        new Tlv(TlvType.RESOURCE_VALUE, null, "value1".getBytes(), 2) }).array();
+        byte[] content = TlvEncoder.encode(new Tlv[] { new Tlv(TlvType.RESOURCE_VALUE, null, "value1".getBytes(), 1),
+                                new Tlv(TlvType.RESOURCE_VALUE, null, "value1".getBytes(), 2) })
+                .array();
 
-        LwM2mNodeDecoder.decode(content, ContentFormat.TLV, new LwM2mPath(9), model);
+        decoder.decode(content, ContentFormat.TLV, new LwM2mPath(9), model);
     }
 
     @Test
     public void tlv_unknown_object__missing_instance_tlv() throws InvalidValueException {
 
-        byte[] content = TlvEncoder.encode(
-                new Tlv[] { new Tlv(TlvType.RESOURCE_VALUE, null, "value1".getBytes(), 1),
-                                        new Tlv(TlvType.RESOURCE_VALUE, null, "value1".getBytes(), 2) }).array();
+        byte[] content = TlvEncoder.encode(new Tlv[] { new Tlv(TlvType.RESOURCE_VALUE, null, "value1".getBytes(), 1),
+                                new Tlv(TlvType.RESOURCE_VALUE, null, "value1".getBytes(), 2) })
+                .array();
 
-        LwM2mObject obj = (LwM2mObject) LwM2mNodeDecoder
-                .decode(content, ContentFormat.TLV, new LwM2mPath(10234), model);
+        LwM2mObject obj = (LwM2mObject) decoder.decode(content, ContentFormat.TLV, new LwM2mPath(10234), model);
 
         assertEquals(1, obj.getInstances().size());
         assertEquals(2, obj.getInstance(0).getResources().size());
@@ -242,7 +245,7 @@ public class LwM2mNodeDecoderTest {
         b.append("{\"n\":\"14\",\"sv\":\"+02:00\"},");
         b.append("{\"n\":\"15\",\"sv\":\"U\"}]}");
 
-        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) LwM2mNodeDecoder.decode(b.toString().getBytes(),
+        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) decoder.decode(b.toString().getBytes(),
                 ContentFormat.JSON, new LwM2mPath(3, 0), model);
 
         assertDeviceInstance(oInstance);
@@ -271,7 +274,7 @@ public class LwM2mNodeDecoderTest {
         b.append("{\"n\":\"3/0/14\",\"sv\":\"+02:00\"},");
         b.append("{\"n\":\"3/0/15\",\"sv\":\"U\"}]}");
 
-        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) LwM2mNodeDecoder.decode(b.toString().getBytes(),
+        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) decoder.decode(b.toString().getBytes(),
                 ContentFormat.JSON, new LwM2mPath(3, 0), model);
 
         assertDeviceInstance(oInstance);
@@ -285,7 +288,7 @@ public class LwM2mNodeDecoderTest {
         b.append("{\"n\":\"0\",\"sv\":\"a string\"},");
         b.append("{\"n\":\"1\",\"v\":10.5},");
         b.append("{\"n\":\"2\",\"bv\":true}]}");
-        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) LwM2mNodeDecoder.decode(b.toString().getBytes(),
+        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) decoder.decode(b.toString().getBytes(),
                 ContentFormat.JSON, new LwM2mPath(1024, 0), model);
 
         assertEquals(0, oInstance.getId());
@@ -294,4 +297,84 @@ public class LwM2mNodeDecoderTest {
         assertEquals(10.5, oInstance.getResource(1).getValue());
         assertEquals(true, oInstance.getResource(2).getValue());
     }
+
+    @Test
+    public void json_timestamped_resources() throws InvalidValueException {
+        // json content for instance 0 of device object
+        StringBuilder b = new StringBuilder();
+        b.append("{\"e\":[");
+        b.append("{\"n\":\"\",\"v\":22.9,\"t\":-30},");
+        b.append("{\"n\":\"\",\"v\":22.4,\"t\":-5},");
+        b.append("{\"n\":\"\",\"v\":24.1,\"t\":-50}],");
+        b.append("\"bt\":25462634}");
+
+        List<TimestampedLwM2mNode> timestampedResources = decoder.decodeTimestampedData(b.toString().getBytes(),
+                ContentFormat.JSON, new LwM2mPath(1024, 0, 1), model);
+
+        assertEquals(3, timestampedResources.size());
+        assertEquals(new Long(25462634L - 5), timestampedResources.get(0).getTimestamp());
+        assertEquals(22.4d, ((LwM2mResource) timestampedResources.get(0).getNode()).getValue());
+        assertEquals(new Long(25462634L - 30), timestampedResources.get(1).getTimestamp());
+        assertEquals(22.9d, ((LwM2mResource) timestampedResources.get(1).getNode()).getValue());
+        assertEquals(new Long(25462634 - 50), timestampedResources.get(2).getTimestamp());
+        assertEquals(24.1d, ((LwM2mResource) timestampedResources.get(2).getNode()).getValue());
+    }
+
+    @Test
+    public void json_timestamped_instances() throws InvalidValueException {
+        // json content for instance 0 of device object
+        StringBuilder b = new StringBuilder();
+        b.append("{\"e\":[");
+        b.append("{\"n\":\"1\",\"v\":22.9,\"t\":-30},");
+        b.append("{\"n\":\"1\",\"v\":22.4,\"t\":-5},");
+        b.append("{\"n\":\"0\",\"sv\":\"a string\",\"t\":-5},");
+        b.append("{\"n\":\"1\",\"v\":24.1,\"t\":-50}],");
+        b.append("\"bt\":25462634}");
+
+        List<TimestampedLwM2mNode> timestampedResources = decoder.decodeTimestampedData(b.toString().getBytes(),
+                ContentFormat.JSON, new LwM2mPath(1024, 0), model);
+
+        assertEquals(3, timestampedResources.size());
+        assertEquals(new Long(25462634L - 5), timestampedResources.get(0).getTimestamp());
+        assertEquals("a string",
+                ((LwM2mObjectInstance) timestampedResources.get(0).getNode()).getResource(0).getValue());
+        assertEquals(22.4d, ((LwM2mObjectInstance) timestampedResources.get(0).getNode()).getResource(1).getValue());
+
+        assertEquals(new Long(25462634L - 30), timestampedResources.get(1).getTimestamp());
+        assertEquals(22.9d, ((LwM2mObjectInstance) timestampedResources.get(1).getNode()).getResource(1).getValue());
+
+        assertEquals(new Long(25462634 - 50), timestampedResources.get(2).getTimestamp());
+        assertEquals(24.1d, ((LwM2mObjectInstance) timestampedResources.get(2).getNode()).getResource(1).getValue());
+    }
+
+    @Test
+    public void json_timestamped_Object() throws InvalidValueException {
+        // json content for instance 0 of device object
+        StringBuilder b = new StringBuilder();
+        b.append("{\"e\":[");
+        b.append("{\"n\":\"0/1\",\"v\":22.9,\"t\":-30},");
+        b.append("{\"n\":\"0/1\",\"v\":22.4,\"t\":-5},");
+        b.append("{\"n\":\"0/0\",\"sv\":\"a string\",\"t\":-5},");
+        b.append("{\"n\":\"1/1\",\"v\":23,\"t\":-5},");
+        b.append("{\"n\":\"0/1\",\"v\":24.1,\"t\":-50}],");
+        b.append("\"bt\":25462634}");
+
+        List<TimestampedLwM2mNode> timestampedResources = decoder.decodeTimestampedData(b.toString().getBytes(),
+                ContentFormat.JSON, new LwM2mPath(1024), model);
+
+        assertEquals(3, timestampedResources.size());
+        assertEquals(new Long(25462634L - 5), timestampedResources.get(0).getTimestamp());
+        assertEquals(22.4d,
+                ((LwM2mObject) timestampedResources.get(0).getNode()).getInstance(0).getResource(1).getValue());
+        assertEquals("a string",
+                ((LwM2mObject) timestampedResources.get(0).getNode()).getInstance(0).getResource(0).getValue());
+        assertEquals(23.0d,
+                ((LwM2mObject) timestampedResources.get(0).getNode()).getInstance(1).getResource(1).getValue());
+        assertEquals(new Long(25462634L - 30), timestampedResources.get(1).getTimestamp());
+        assertEquals(22.9d,
+                ((LwM2mObject) timestampedResources.get(1).getNode()).getInstance(0).getResource(1).getValue());
+        assertEquals(new Long(25462634 - 50), timestampedResources.get(2).getTimestamp());
+        assertEquals(24.1d,
+                ((LwM2mObject) timestampedResources.get(2).getNode()).getInstance(0).getResource(1).getValue());
+    }
 }
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 a3b674db..40bc25dd 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
@@ -20,6 +20,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
@@ -30,6 +31,7 @@ import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.util.Charsets;
 import org.junit.Assert;
@@ -42,16 +44,18 @@ import org.junit.Test;
 public class LwM2mNodeEncoderTest {
 
     private static LwM2mModel model;
+    private static LwM2mNodeEncoder encoder;
 
     @BeforeClass
     public static void loadModel() {
         model = new LwM2mModel(ObjectLoader.loadDefault());
+        encoder = new DefaultLwM2mNodeEncoder();
     }
 
     @Test
     public void text_encode_single_resource() {
 
-        byte[] encoded = LwM2mNodeEncoder.encode(LwM2mSingleResource.newFloatResource(15, 56.4D), ContentFormat.TEXT,
+        byte[] encoded = encoder.encode(LwM2mSingleResource.newFloatResource(15, 56.4D), ContentFormat.TEXT,
                 new LwM2mPath("/323/0/15"), model);
 
         Assert.assertEquals("56.4", new String(encoded, Charsets.UTF_8));
@@ -60,9 +64,8 @@ public class LwM2mNodeEncoderTest {
     @Test
     public void text_encode_date_as_long() {
 
-        byte[] encoded = LwM2mNodeEncoder.encode(
-                LwM2mSingleResource.newStringResource(13, "2010-01-01T12:00:00+01:00"), ContentFormat.TEXT,
-                new LwM2mPath("/3/0/13"), model);
+        byte[] encoded = encoder.encode(LwM2mSingleResource.newStringResource(13, "2010-01-01T12:00:00+01:00"),
+                ContentFormat.TEXT, new LwM2mPath("/3/0/13"), model);
 
         Assert.assertEquals("1262343600", new String(encoded, Charsets.UTF_8));
     }
@@ -70,8 +73,8 @@ public class LwM2mNodeEncoderTest {
     @Test
     public void text_encode_date_as_iso_string() {
 
-        byte[] encoded = LwM2mNodeEncoder.encode(LwM2mSingleResource.newIntegerResource(13, 1367491215000L),
-                ContentFormat.TEXT, new LwM2mPath("/3/0/13"), model);
+        byte[] encoded = encoder.encode(LwM2mSingleResource.newIntegerResource(13, 1367491215000L), ContentFormat.TEXT,
+                new LwM2mPath("/3/0/13"), model);
 
         Assert.assertEquals("1367491215", new String(encoded, Charsets.UTF_8));
     }
@@ -81,8 +84,8 @@ public class LwM2mNodeEncoderTest {
         Map<Integer, Long> values = new HashMap<>();
         values.put(0, 1L);
         values.put(1, 5L);
-        LwM2mNodeEncoder.encode(LwM2mMultipleResource.newIntegerResource(6, values), ContentFormat.TEXT, new LwM2mPath(
-                "/3/0/6"), model);
+        encoder.encode(LwM2mMultipleResource.newIntegerResource(6, values), ContentFormat.TEXT, new LwM2mPath("/3/0/6"),
+                model);
     }
 
     // device instance encoded as an array of resources
@@ -129,7 +132,7 @@ public class LwM2mNodeEncoderTest {
     @Test
     public void tlv_encode_device_object_instance_as_resources_array() {
         LwM2mObjectInstance oInstance = new LwM2mObjectInstance(0, getDeviceResources());
-        byte[] encoded = LwM2mNodeEncoder.encode(oInstance, ContentFormat.TLV, new LwM2mPath("/3/0"), model);
+        byte[] encoded = encoder.encode(oInstance, ContentFormat.TLV, new LwM2mPath("/3/0"), model);
 
         Assert.assertArrayEquals(ENCODED_DEVICE, encoded);
     }
@@ -137,7 +140,7 @@ public class LwM2mNodeEncoderTest {
     @Test
     public void tlv_encode_device_object_instance_as_resources_array__undefined_instance_id() {
         LwM2mObjectInstance oInstance = new LwM2mObjectInstance(LwM2mObjectInstance.UNDEFINED, getDeviceResources());
-        byte[] encoded = LwM2mNodeEncoder.encode(oInstance, ContentFormat.TLV, new LwM2mPath("/3"), model);
+        byte[] encoded = encoder.encode(oInstance, ContentFormat.TLV, new LwM2mPath("/3"), model);
 
         Assert.assertArrayEquals(ENCODED_DEVICE, encoded);
     }
@@ -145,7 +148,7 @@ public class LwM2mNodeEncoderTest {
     @Test
     public void tlv_encode_device_object_instance_as_instance() {
         LwM2mObjectInstance oInstance = new LwM2mObjectInstance(0, getDeviceResources());
-        byte[] encoded = LwM2mNodeEncoder.encode(oInstance, ContentFormat.TLV, new LwM2mPath("/3"), model);
+        byte[] encoded = encoder.encode(oInstance, ContentFormat.TLV, new LwM2mPath("/3"), model);
 
         // TLV instance = { type=INSTANCE, instanceId=0, length=DEVICE_ENCODED.lentgh, value=DEVICE_ENCODED }
         byte[] instanceTlv = new byte[ENCODED_DEVICE.length + 3];
@@ -159,7 +162,7 @@ public class LwM2mNodeEncoderTest {
     public void tlv_encode_device_object() {
 
         LwM2mObject object = new LwM2mObject(3, new LwM2mObjectInstance(0, getDeviceResources()));
-        byte[] encoded = LwM2mNodeEncoder.encode(object, ContentFormat.TLV, new LwM2mPath("/3"), model);
+        byte[] encoded = encoder.encode(object, ContentFormat.TLV, new LwM2mPath("/3"), model);
 
         // encoded as an array of resource TLVs
         Assert.assertArrayEquals(ENCODED_DEVICE, encoded);
@@ -169,7 +172,7 @@ public class LwM2mNodeEncoderTest {
     public void json_encode_device_object_instance() {
 
         LwM2mObjectInstance oInstance = new LwM2mObjectInstance(0, getDeviceResources());
-        byte[] encoded = LwM2mNodeEncoder.encode(oInstance, ContentFormat.JSON, new LwM2mPath("/3/0"), model);
+        byte[] encoded = encoder.encode(oInstance, ContentFormat.JSON, new LwM2mPath("/3/0"), model);
 
         StringBuilder b = new StringBuilder();
         b.append("{\"e\":[");
@@ -193,4 +196,82 @@ public class LwM2mNodeEncoderTest {
         String expected = b.toString();
         Assert.assertEquals(expected, new String(encoded));
     }
+
+    @Test
+    public void json_encode_timestamped_resources() throws InvalidValueException {
+        List<TimestampedLwM2mNode> data = new ArrayList<>();
+        data.add(new TimestampedLwM2mNode(500L, LwM2mSingleResource.newFloatResource(1, 22.9)));
+        data.add(new TimestampedLwM2mNode(510L, LwM2mSingleResource.newFloatResource(1, 22.4)));
+        data.add(new TimestampedLwM2mNode(520L, LwM2mSingleResource.newFloatResource(1, 24.1)));
+
+        byte[] encoded = encoder.encodeTimestampedData(data, ContentFormat.JSON, new LwM2mPath(1024, 0, 1), model);
+
+        StringBuilder b = new StringBuilder();
+        b.append("{\"e\":[");
+        b.append("{\"n\":\"\",\"v\":22.9,\"t\":500},");
+        b.append("{\"n\":\"\",\"v\":22.4,\"t\":510},");
+        b.append("{\"n\":\"\",\"v\":24.1,\"t\":520}]}");
+
+        String expected = b.toString();
+        Assert.assertEquals(expected, new String(encoded));
+    }
+
+    @Test
+    public void json_encode_timestamped_instances() throws InvalidValueException {
+        List<TimestampedLwM2mNode> data = new ArrayList<>();
+
+        LwM2mObjectInstance instanceAt110 = new LwM2mObjectInstance(0, LwM2mSingleResource.newFloatResource(1, 22.9));
+        LwM2mObjectInstance instanceAt120 = new LwM2mObjectInstance(0, LwM2mSingleResource.newFloatResource(1, 22.4),
+                LwM2mSingleResource.newStringResource(0, "a string"));
+        LwM2mObjectInstance instanceAt130 = new LwM2mObjectInstance(0, LwM2mSingleResource.newFloatResource(1, 24.1));
+
+        data.add(new TimestampedLwM2mNode(110L, instanceAt110));
+        data.add(new TimestampedLwM2mNode(120L, instanceAt120));
+        data.add(new TimestampedLwM2mNode(130L, instanceAt130));
+
+        byte[] encoded = encoder.encodeTimestampedData(data, ContentFormat.JSON, new LwM2mPath(1024, 0), model);
+
+        StringBuilder b = new StringBuilder();
+        b.append("{\"e\":[");
+        b.append("{\"n\":\"1\",\"v\":22.9,\"t\":110},");
+        b.append("{\"n\":\"0\",\"sv\":\"a string\",\"t\":120},");
+        b.append("{\"n\":\"1\",\"v\":22.4,\"t\":120},");
+        b.append("{\"n\":\"1\",\"v\":24.1,\"t\":130}]}");
+
+        String expected = b.toString();
+        Assert.assertEquals(expected, new String(encoded));
+    }
+
+    @Test
+    public void json_encode_timestamped_Object() throws InvalidValueException {
+        List<TimestampedLwM2mNode> data = new ArrayList<>();
+
+        LwM2mObject objectAt210 = new LwM2mObject(1204,
+                new LwM2mObjectInstance(0, LwM2mSingleResource.newFloatResource(1, 22.9)));
+
+        LwM2mObject objectAt220 = new LwM2mObject(1204,
+                new LwM2mObjectInstance(0, LwM2mSingleResource.newFloatResource(1, 22.4),
+                        LwM2mSingleResource.newStringResource(0, "a string")),
+                new LwM2mObjectInstance(1, LwM2mSingleResource.newFloatResource(1, 23)));
+
+        LwM2mObject objetAt230 = new LwM2mObject(1204,
+                new LwM2mObjectInstance(0, LwM2mSingleResource.newFloatResource(1, 24.1)));
+
+        data.add(new TimestampedLwM2mNode(210L, objectAt210));
+        data.add(new TimestampedLwM2mNode(220L, objectAt220));
+        data.add(new TimestampedLwM2mNode(230L, objetAt230));
+
+        byte[] encoded = encoder.encodeTimestampedData(data, ContentFormat.JSON, new LwM2mPath(1024), model);
+
+        StringBuilder b = new StringBuilder();
+        b.append("{\"e\":[");
+        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.0,\"t\":220},");
+        b.append("{\"n\":\"0/1\",\"v\":24.1,\"t\":230}]}");
+
+        String expected = b.toString();
+        Assert.assertEquals(expected, new String(encoded));
+    }
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/tlv/TlvDecoderTest.java b/leshan-core/src/test/java/org/eclipse/leshan/tlv/TlvDecoderTest.java
index a504e45e..6911a9ca 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/tlv/TlvDecoderTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/tlv/TlvDecoderTest.java
@@ -15,9 +15,7 @@
  *******************************************************************************/
 package org.eclipse.leshan.tlv;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.*;
 
 import java.nio.ByteBuffer;
 import java.util.Arrays;
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 57db860f..6ec21f65 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
@@ -19,6 +19,7 @@ package org.eclipse.leshan.integration.tests;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -28,6 +29,7 @@ import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.observation.Observation;
 import org.eclipse.leshan.core.request.ObserveRequest;
 import org.eclipse.leshan.core.request.ReadRequest;
@@ -151,7 +153,8 @@ public class ObserveTest {
         private LwM2mNode content;
 
         @Override
-        public void newValue(final Observation observation, final LwM2mNode value) {
+        public void newValue(final Observation observation, final LwM2mNode value,
+                List<TimestampedLwM2mNode> timestampedValues) {
             receivedNotify.set(true);
             content = value;
             latch.countDown();
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java
index 0f35405f..8982a930 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeIntegrationTestHelper.java
@@ -34,6 +34,10 @@ import org.eclipse.leshan.client.object.Security;
 import org.eclipse.leshan.client.object.Server;
 import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
 import org.eclipse.leshan.client.resource.ObjectsInitializer;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.BindingMode;
 import org.eclipse.leshan.core.response.ExecuteResponse;
 import org.eclipse.leshan.integration.tests.util.QueueModeLeshanServer;
@@ -72,12 +76,10 @@ public class QueueModeIntegrationTestHelper extends IntegrationTestHelper {
         networkConfig = new NetworkConfig();
         networkConfig.setLong(Keys.ACK_TIMEOUT, ACK_TIMEOUT);
         networkConfig.setInt(Keys.MAX_RETRANSMIT, 0);
-        noSecureEndpoint = new CoapEndpoint(
-                new InetSocketAddress(InetAddress.getLoopbackAddress(), networkConfig.getInt(Keys.COAP_PORT)),
-                networkConfig);
-        secureEndpoint = new CoapEndpoint(
-                new InetSocketAddress(InetAddress.getLoopbackAddress(), networkConfig.getInt(Keys.COAP_SECURE_PORT)),
-                networkConfig);
+        noSecureEndpoint = new CoapEndpoint(new InetSocketAddress(InetAddress.getLoopbackAddress(),
+                networkConfig.getInt(Keys.COAP_PORT)), networkConfig);
+        secureEndpoint = new CoapEndpoint(new InetSocketAddress(InetAddress.getLoopbackAddress(),
+                networkConfig.getInt(Keys.COAP_SECURE_PORT)), networkConfig);
     }
 
     private void createCoapServer(ClientRegistry clientRegistry, SecurityStore securityStore) {
@@ -111,16 +113,18 @@ public class QueueModeIntegrationTestHelper extends IntegrationTestHelper {
         createCoapServer(clientRegistry, securityRegistry);
         InMemoryMessageStore inMemoryMessageStore = new InMemoryMessageStore();
         LwM2mModelProvider modelProvider = new StandardModelProvider();
+        LwM2mNodeEncoder encoder = new DefaultLwM2mNodeEncoder();
+        LwM2mNodeDecoder decoder = new DefaultLwM2mNodeDecoder();
         CaliforniumObservationRegistryImpl observationRegistry = new CaliforniumObservationRegistryImpl(
-                new InMemoryObservationStore(), clientRegistry, modelProvider);
+                new InMemoryObservationStore(), clientRegistry, modelProvider, decoder);
         observationRegistry.setSecureEndpoint(secureEndpoint);
         secureEndpoint.addNotificationListener(observationRegistry);
         observationRegistry.setNonSecureEndpoint(noSecureEndpoint);
         noSecureEndpoint.addNotificationListener(observationRegistry);
         LwM2mRequestSender delegateSender = new CaliforniumLwM2mRequestSender(new HashSet<>(coapServer.getEndpoints()),
-                observationRegistry, modelProvider);
-        LwM2mRequestSender secondDelegateSender = new CaliforniumLwM2mRequestSender(
-                new HashSet<>(coapServer.getEndpoints()), observationRegistry, modelProvider);
+                observationRegistry, modelProvider, encoder, decoder);
+        LwM2mRequestSender secondDelegateSender = new CaliforniumLwM2mRequestSender(new HashSet<>(
+                coapServer.getEndpoints()), observationRegistry, modelProvider, encoder, decoder);
         QueuedRequestSender queueRequestSender = QueuedRequestSender.builder().setMessageStore(inMemoryMessageStore)
                 .setRequestSender(secondDelegateSender).setClientRegistry(clientRegistry)
                 .setObservationRegistry(observationRegistry).build();
@@ -137,12 +141,13 @@ public class QueueModeIntegrationTestHelper extends IntegrationTestHelper {
 
     public QueuedModeLeshanClient createClient(long lifeTime) {
         ObjectsInitializer initializer = new ObjectsInitializer();
-        initializer.setInstancesForObject(LwM2mId.SECURITY,
+        initializer.setInstancesForObject(
+                LwM2mId.SECURITY,
                 Security.noSec("coap://" + noSecureEndpoint.getAddress().getHostString() + ":"
                         + noSecureEndpoint.getAddress().getPort(), 12345));
         if (lifeTime == 0) {
-            initializer.setInstancesForObject(LwM2mId.SERVER,
-                    new Server(12345, CUSTOM_LIFETIME, BindingMode.UQ, false));
+            initializer
+                    .setInstancesForObject(LwM2mId.SERVER, new Server(12345, CUSTOM_LIFETIME, BindingMode.UQ, false));
         } else {
             initializer.setInstancesForObject(LwM2mId.SERVER, new Server(12345, lifeTime, BindingMode.UQ, false));
         }
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java
index fbf2cb3c..313b6fcd 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/QueueModeTest.java
@@ -20,9 +20,7 @@
  *******************************************************************************/
 package org.eclipse.leshan.integration.tests;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
+import static org.junit.Assert.*;
 
 import java.util.List;
 import java.util.concurrent.CountDownLatch;
@@ -32,6 +30,7 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import org.eclipse.californium.core.server.resources.CoapExchange;
 import org.eclipse.leshan.ResponseCode;
 import org.eclipse.leshan.core.node.LwM2mNode;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.observation.Observation;
 import org.eclipse.leshan.core.request.ObserveRequest;
 import org.eclipse.leshan.core.request.ReadRequest;
@@ -101,8 +100,8 @@ public class QueueModeTest {
     @Test
     public void first_request_sent_immediately() throws Exception {
         createAndAddResponseListener(countDownLatch);
-        helper.server.getLwM2mRequestSender().send(helper.getClient(), TEST_REQUEST_TICKET + "1",
-                new ReadRequest(3, 0));
+        helper.server.getLwM2mRequestSender()
+                .send(helper.getClient(), TEST_REQUEST_TICKET + "1", new ReadRequest(3, 0));
         if (!countDownLatch.await(2, TimeUnit.SECONDS)) {
             fail("response from client was not received within timeout");
         }
@@ -116,8 +115,8 @@ public class QueueModeTest {
 
         createAndAddResponseListener(countDownLatch);
 
-        helper.server.getLwM2mRequestSender().send(helper.getClient(), TEST_REQUEST_TICKET + "1",
-                new ReadRequest(3, 0));
+        helper.server.getLwM2mRequestSender()
+                .send(helper.getClient(), TEST_REQUEST_TICKET + "1", new ReadRequest(3, 0));
         // assert that queue has one request left in processing state still
         assertQueueHasMessageCount(1, 5000);
     }
@@ -149,8 +148,8 @@ public class QueueModeTest {
         }
 
         // assert that queue has one additional new request
-        List<QueuedRequest> requests = ((InMemoryMessageStore) helper.server.getMessageStore())
-                .retrieveAll(helper.getClient().getEndpoint());
+        List<QueuedRequest> requests = ((InMemoryMessageStore) helper.server.getMessageStore()).retrieveAll(helper
+                .getClient().getEndpoint());
         assertEquals(2, requests.size());
     }
 
@@ -468,9 +467,8 @@ public class QueueModeTest {
             InMemoryMessageStore messageStore = (InMemoryMessageStore) helper.server.getMessageStore();
             queuedRequestCount = messageStore.retrieveAll(helper.getClient().getEndpoint()).size();
         } while (queuedRequestCount < count && duration <= timeout);
-        assertTrue(
-                "Expected to have at least " + count + " queued request in queue but have [" + queuedRequestCount + "]",
-                queuedRequestCount == count);
+        assertTrue("Expected to have at least " + count + " queued request in queue but have [" + queuedRequestCount
+                + "]", queuedRequestCount == count);
     }
 
     private void assertQueueIsEmpty(long timeout) throws InterruptedException {
@@ -498,7 +496,8 @@ public class QueueModeTest {
         private final AtomicBoolean receivedNotify = new AtomicBoolean();
 
         @Override
-        public void newValue(Observation observation, LwM2mNode value) {
+        public void newValue(Observation observation, LwM2mNode mostRecentvalue,
+                List<TimestampedLwM2mNode> timestampedValues) {
             receivedNotify.set(true);
             latch.countDown();
         }
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/QueuedModeLeshanClient.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/QueuedModeLeshanClient.java
index 26a81398..fa123696 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/QueuedModeLeshanClient.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/util/QueuedModeLeshanClient.java
@@ -39,6 +39,8 @@ import org.eclipse.leshan.client.californium.impl.SecurityObjectPskStore;
 import org.eclipse.leshan.client.resource.LwM2mObjectEnabler;
 import org.eclipse.leshan.client.servers.BootstrapHandler;
 import org.eclipse.leshan.client.servers.RegistrationEngine;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
 import org.eclipse.leshan.util.Validate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -65,7 +67,7 @@ public class QueuedModeLeshanClient implements LwM2mClient {
         private LwM2mObjectEnabler nodeEnabler;
 
         public CustomObjectResource(LwM2mObjectEnabler nodeEnabler) {
-            super(nodeEnabler, bootstrapHandler);
+            super(nodeEnabler, bootstrapHandler, new DefaultLwM2mNodeEncoder(), new DefaultLwM2mNodeDecoder());
             this.nodeEnabler = nodeEnabler;
             this.nodeEnabler.setNotifySender(this);
             setObservable(true);
@@ -91,8 +93,8 @@ public class QueuedModeLeshanClient implements LwM2mClient {
         boolean handleGet(CoapExchange coapExchange);
     }
 
-    public QueuedModeLeshanClient(String endpoint, InetSocketAddress localAddress, InetSocketAddress localSecureAddress,
-            List<? extends LwM2mObjectEnabler> objectEnablers) {
+    public QueuedModeLeshanClient(String endpoint, InetSocketAddress localAddress,
+            InetSocketAddress localSecureAddress, List<? extends LwM2mObjectEnabler> objectEnablers) {
         Validate.notNull(localAddress);
         Validate.notNull(localSecureAddress);
         Validate.notNull(objectEnablers);
@@ -102,8 +104,8 @@ public class QueuedModeLeshanClient implements LwM2mClient {
         this.objectEnablers = new ConcurrentHashMap<>();
         for (LwM2mObjectEnabler enabler : objectEnablers) {
             if (this.objectEnablers.containsKey(enabler.getId())) {
-                throw new IllegalArgumentException(
-                        String.format("There is several objectEnablers with the same id %d.", enabler.getId()));
+                throw new IllegalArgumentException(String.format(
+                        "There is several objectEnablers with the same id %d.", enabler.getId()));
             }
             this.objectEnablers.put(enabler.getId(), enabler);
         }
diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationTest.java
index 1a793b93..d67e219f 100644
--- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationTest.java
+++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CaliforniumObservationTest.java
@@ -22,6 +22,7 @@ import java.util.concurrent.ThreadLocalRandom;
 import org.eclipse.californium.core.coap.Request;
 import org.eclipse.californium.core.observe.InMemoryObservationStore;
 import org.eclipse.leshan.core.node.LwM2mPath;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeDecoder;
 import org.eclipse.leshan.core.observation.Observation;
 import org.eclipse.leshan.server.californium.CaliforniumObservationRegistry;
 import org.eclipse.leshan.server.impl.ClientRegistryImpl;
@@ -42,7 +43,7 @@ public class CaliforniumObservationTest {
     public void setUp() throws Exception {
         support.givenASimpleClient();
         registry = new CaliforniumObservationRegistryImpl(new InMemoryObservationStore(), new ClientRegistryImpl(),
-                new StandardModelProvider());
+                new StandardModelProvider(), new DefaultLwM2mNodeDecoder());
     }
 
     @Test
diff --git a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java
index 7bdaab86..df1817b3 100644
--- a/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java
+++ b/leshan-server-cf/src/test/java/org/eclipse/leshan/server/californium/impl/CoapRequestBuilderTest.java
@@ -34,6 +34,8 @@ import org.eclipse.leshan.core.model.LwM2mModel;
 import org.eclipse.leshan.core.model.ObjectLoader;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.request.CreateRequest;
 import org.eclipse.leshan.core.request.DeleteRequest;
@@ -58,10 +60,12 @@ import org.junit.Test;
 public class CoapRequestBuilderTest {
 
     private static LwM2mModel model;
+    private static LwM2mNodeEncoder encoder;
 
     @BeforeClass
     public static void loadModel() {
         model = new LwM2mModel(ObjectLoader.loadDefault());
+        encoder = new DefaultLwM2mNodeEncoder();
     }
 
     private Client newClient() throws UnknownHostException {
@@ -85,7 +89,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         ReadRequest request = new ReadRequest(3, 0);
         builder.visit(request);
 
@@ -103,7 +107,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         ReadRequest request = new ReadRequest(3, 0, 1);
         builder.visit(request);
 
@@ -118,7 +122,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         ReadRequest request = new ReadRequest(3);
         builder.visit(request);
 
@@ -133,7 +137,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         DiscoverRequest request = new DiscoverRequest(3, 0);
         builder.visit(request);
 
@@ -152,7 +156,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         WriteRequest request = new WriteRequest(Mode.UPDATE, 3, 0, LwM2mSingleResource.newStringResource(4, "value"));
         builder.visit(request);
 
@@ -176,7 +180,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         WriteRequest request = new WriteRequest(3, 0, 14, "value");
         builder.visit(request);
 
@@ -191,7 +195,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         WriteAttributesRequest request = new WriteAttributesRequest(3, 0, 14,
                 new ObserveSpec.Builder().minPeriod(10).maxPeriod(100).build());
         builder.visit(request);
@@ -210,7 +214,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         ExecuteRequest request = new ExecuteRequest(3, 0, 12, "params");
         builder.visit(request);
 
@@ -229,7 +233,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         CreateRequest request = new CreateRequest(12, LwM2mSingleResource.newStringResource(0, "value"));
         builder.visit(request);
 
@@ -252,7 +256,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         CreateRequest request = new CreateRequest(12,
                 new LwM2mObjectInstance(26, LwM2mSingleResource.newStringResource(0, "value")));
         builder.visit(request);
@@ -277,7 +281,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         DeleteRequest request = new DeleteRequest(12, 0);
         builder.visit(request);
 
@@ -295,7 +299,7 @@ public class CoapRequestBuilderTest {
 
         // test
         CoapRequestBuilder builder = new CoapRequestBuilder(
-                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model);
+                new InetSocketAddress(client.getAddress(), client.getPort()), client.getRootPath(), model, encoder);
         ObserveRequest request = new ObserveRequest(12, 0);
         builder.visit(request);