Repository: eclipse/leshan
Pull request #911 (2020-10-22 09:52:25)

Add SenML JSON Decoder.
This PR aims to finalize support of SenML JSON begins by @cavenaghi9 long time ago.

It contains : 
1. refactoring code to be able to add a new SenML JSON encoder/decoder based on **jackson**,
2. add LwM2mNodeSenMLDecoder,
3. Enhance number support (especially unsigned it),
4. support of timestamped value,
5. some integration tests refactoring to test codec more easily,
6. support of resource instance operation,
7. support of object link.

sbernard31: About :
> When there is no reference to Object Instance in the TLV/CBOR/JSON payload of the "Create" command, the LwM2M Client MUST assigns the ID of the created Object Instance.

From LWM2M 1.1 [§6.3.6. Create Operation](http://www.openmobilealliance.org/release/LightweightM2M/V1_1_1-20190617-A/HTML-Version/OMA-TS-LightweightM2M_Core-V1_1_1-20190617-A.html#6-3-6-0-636-Create-Operation), for now we will not make this possible as specification is not clear enough : see https://github.com/OpenMobileAlliance/OMA_LwM2M_for_Developers/issues/422.
sbernard31: This PR does not contains support of empty multi instance resource : see #915 
sbernard31: (some relative SenML issue which are not part of this PR : #915, #916,  #917)

Patch:
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 584b7a8a..210a88df 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
@@ -155,6 +155,38 @@ public class LwM2mPath {
         }
     }
 
+    /**
+     * @param start start of the path
+     * @return true if the current path start with the given path
+     */
+    public boolean startWith(LwM2mPath start) {
+        // object id
+        if (start.getObjectId() == null)
+            return true;
+        if (!start.getObjectId().equals(this.getObjectId())) {
+            return false;
+        }
+        // object instance id
+        if (start.getObjectInstanceId() == null)
+            return true;
+        if (!start.getObjectInstanceId().equals(this.getObjectInstanceId())) {
+            return false;
+        }
+        // resource id
+        if (start.getResourceId() == null)
+            return true;
+        if (!start.getResourceId().equals(this.getResourceId())) {
+            return false;
+        }
+        // resource instance id
+        if (start.getResourceInstanceId() == null)
+            return true;
+        if (!start.getResourceInstanceId().equals(this.getResourceInstanceId())) {
+            return false;
+        }
+        return true;
+    }
+
     /**
      * Returns the object ID in the path.
      *
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/ObjectLink.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/ObjectLink.java
index d17daf2a..c8b27fcd 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/ObjectLink.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/ObjectLink.java
@@ -15,6 +15,8 @@
  *******************************************************************************/
 package org.eclipse.leshan.core.node;
 
+import java.util.Arrays;
+
 import org.eclipse.leshan.core.util.Validate;
 
 /**
@@ -68,6 +70,23 @@ public class ObjectLink {
         }
     }
 
+    public String encodeToString() {
+        return this.getObjectId() + ":" + this.getObjectInstanceId();
+    }
+
+    public static ObjectLink decodeFromString(String encodedObjectLink) {
+        String[] intArr = encodedObjectLink.split(":");
+        if (intArr.length != 2)
+            throw new IllegalArgumentException(
+                    String.format("Invalid value %s for objectLink", Arrays.toString(intArr)));
+        try {
+            return new ObjectLink(Integer.parseInt(intArr[0]), Integer.parseInt(intArr[1]));
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException(
+                    String.format("Invalid value %s for objectLink", Arrays.toString(intArr)));
+        }
+    }
+
     public int getObjectId() {
         return objectId;
     }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
index bf2d681e..4eb0f682 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeDecoder.java
@@ -32,10 +32,12 @@ import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 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.senml.LwM2mNodeSenMLJsonDecoder;
 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.core.util.Validate;
+import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -56,6 +58,7 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
         Map<ContentFormat, NodeDecoder> decoders = new HashMap<>();
         decoders.put(ContentFormat.TEXT, new LwM2mNodeTextDecoder());
         decoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueDecoder());
+        decoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLJsonDecoder(new SenMLJsonJacksonEncoderDecoder()));
 
         // tlv
         LwM2mNodeTlvDecoder tlvDecoder = new LwM2mNodeTlvDecoder();
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
index f64b6dfb..ff31a065 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
@@ -31,6 +31,7 @@ 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.core.util.Validate;
+import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -51,7 +52,7 @@ public class DefaultLwM2mNodeEncoder implements LwM2mNodeEncoder {
         Map<ContentFormat, NodeEncoder> encoders = new HashMap<>();
         encoders.put(ContentFormat.TEXT, new LwM2mNodeTextEncoder());
         encoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueEncoder());
-        encoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLJsonEncoder());
+        encoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLJsonEncoder(new SenMLJsonJacksonEncoderDecoder()));
 
         // tlv
         LwM2mNodeTlvEncoder tlvDecoder = new LwM2mNodeTlvEncoder();
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonDecoder.java
new file mode 100644
index 00000000..b6944128
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonDecoder.java
@@ -0,0 +1,470 @@
+/*******************************************************************************
+ * 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:
+ *     Boya Zhang - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.node.codec.senml;
+
+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;
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.node.LwM2mMultipleResource;
+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.LwM2mResourceInstance;
+import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.ObjectLink;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
+import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.core.node.codec.TimestampedNodeDecoder;
+import org.eclipse.leshan.core.util.Base64;
+import org.eclipse.leshan.core.util.datatype.NumberUtil;
+import org.eclipse.leshan.core.util.datatype.ULong;
+import org.eclipse.leshan.senml.SenMLDecoder;
+import org.eclipse.leshan.senml.SenMLException;
+import org.eclipse.leshan.senml.SenMLPack;
+import org.eclipse.leshan.senml.SenMLRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LwM2mNodeSenMLJsonDecoder implements TimestampedNodeDecoder {
+
+    private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeSenMLJsonDecoder.class);
+
+    private final SenMLDecoder decoder;
+
+    public LwM2mNodeSenMLJsonDecoder(SenMLDecoder decoder) {
+        this.decoder = decoder;
+    }
+
+    @SuppressWarnings("unchecked")
+    @Override
+    public <T extends LwM2mNode> T decode(byte[] content, LwM2mPath path, LwM2mModel model, Class<T> nodeClass)
+            throws CodecException {
+        try {
+            SenMLPack pack = decoder.fromSenML(content);
+            List<TimestampedLwM2mNode> timestampedNodes = parseSenMLPack(pack, path, model, nodeClass);
+            if (timestampedNodes.size() == 0) {
+                return null;
+            } else if (timestampedNodes.size() == 1 && timestampedNodes.get(0).getTimestamp() == null) {
+                return (T) timestampedNodes.get(0).getNode();
+            } else {
+                throw new CodecException("Unable to decode node[path:%s] : value should not be timestamped", path);
+            }
+        } catch (SenMLException e) {
+            String jsonStrValue = content != null ? new String(content) : "";
+            throw new CodecException(e, "Unable to decode node[path:%s] : %s", path, jsonStrValue, e);
+        }
+    }
+
+    @Override
+    public List<TimestampedLwM2mNode> decodeTimestampedData(byte[] content, LwM2mPath path, LwM2mModel model,
+            Class<? extends LwM2mNode> nodeClass) throws CodecException {
+        try {
+            SenMLPack pack = decoder.fromSenML(content);
+            return parseSenMLPack(pack, path, model, nodeClass);
+        } catch (SenMLException e) {
+            String jsonStrValue = content != null ? new String(content) : "";
+            throw new CodecException(e, "Unable to decode node[path:%s] : %s", path, jsonStrValue, e);
+        }
+    }
+
+    private List<TimestampedLwM2mNode> parseSenMLPack(SenMLPack pack, LwM2mPath path, LwM2mModel model,
+            Class<? extends LwM2mNode> nodeClass) throws CodecException {
+
+        LOG.trace("Parsing SenML JSON object for path {}: {}", path, pack);
+
+        // Group Records by time-stamp
+        Map<Long, Collection<ResolvedSenMLRecord>> recordsByTimestamp = groupRecordByTimestamp(pack.getRecords(), path);
+
+        // fill time-stamped nodes collection
+        List<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>();
+        for (Entry<Long, Collection<ResolvedSenMLRecord>> entryByTimestamp : recordsByTimestamp.entrySet()) {
+
+            // Group records by instance
+            Map<Integer, Collection<ResolvedSenMLRecord>> recordsByInstanceId = groupRecordsByInstanceId(
+                    entryByTimestamp.getValue());
+
+            // Create lwm2m node
+            LwM2mNode node = null;
+            if (nodeClass == LwM2mObject.class) {
+                Collection<LwM2mObjectInstance> instances = new ArrayList<>();
+                for (Entry<Integer, Collection<ResolvedSenMLRecord>> entryByInstanceId : recordsByInstanceId
+                        .entrySet()) {
+                    Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(entryByInstanceId.getValue(), path,
+                            model);
+
+                    instances.add(new LwM2mObjectInstance(entryByInstanceId.getKey(), resourcesMap.values()));
+                }
+
+                node = new LwM2mObject(path.getObjectId(), instances);
+            } else if (nodeClass == LwM2mObjectInstance.class) {
+                // validate we have resources for only 1 instance
+                if (recordsByInstanceId.size() != 1)
+                    throw new CodecException("One instance expected in the payload [path:%s]", path);
+
+                // Extract resources
+                Entry<Integer, Collection<ResolvedSenMLRecord>> instanceEntry = recordsByInstanceId.entrySet()
+                        .iterator().next();
+                Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(instanceEntry.getValue(), path, model);
+
+                // Create instance
+                node = new LwM2mObjectInstance(instanceEntry.getKey(), resourcesMap.values());
+            } else if (nodeClass == LwM2mResource.class) {
+                // validate we have resources for only 1 instance
+                if (recordsByInstanceId.size() > 1)
+                    throw new CodecException("Only one instance expected in the payload [path:%s]", path);
+
+                // Extract resources
+                Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(
+                        recordsByInstanceId.values().iterator().next(), path, model);
+
+                // validate there is only 1 resource
+                if (resourcesMap.size() != 1)
+                    throw new CodecException("One resource should be present in the payload [path:%s]", path);
+
+                node = resourcesMap.values().iterator().next();
+            } else if (nodeClass == LwM2mResourceInstance.class) {
+                // validate we have resources for only 1 instance
+                if (recordsByInstanceId.size() > 1)
+                    throw new CodecException("Only one instance expected in the payload [path:%s]", path);
+
+                // Extract resources
+                Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(
+                        recordsByInstanceId.values().iterator().next(), path, model);
+
+                // validate there is only 1 resource
+                if (resourcesMap.size() != 1)
+                    throw new CodecException("One resource should be present in the payload [path:%s]", path);
+
+                LwM2mResource resource = resourcesMap.values().iterator().next();
+                if (!resource.isMultiInstances()) {
+                    throw new CodecException("Resource should be multi Instances resource [path:%s]", path);
+                }
+
+                if (resource.getInstances().isEmpty()) {
+                    throw new CodecException("Resource instances should not be not empty [path:%s]", path);
+                }
+
+                if (resource.getInstances().size() > 1) {
+                    throw new CodecException("Resource instances should not be > 1 [path:%s]", path);
+                }
+
+                node = resourcesMap.values().iterator().next().getInstance(path.getResourceInstanceId());
+            } else {
+                throw new IllegalArgumentException("invalid node class: " + nodeClass);
+            }
+
+            // add time-stamped node
+            timestampedNodes.add(new TimestampedLwM2mNode(entryByTimestamp.getKey(), node));
+        }
+
+        return timestampedNodes;
+
+    }
+
+    /**
+     * Resolved record then group it by time-stamp
+     * 
+     * @return a sorted map (timestamp => collection of record) order by descending time-stamp (most recent one at first
+     *         place). If null time-stamp (meaning no time information) exists it always at first place.
+     */
+    private SortedMap<Long, Collection<ResolvedSenMLRecord>> groupRecordByTimestamp(List<SenMLRecord> records,
+            LwM2mPath requestPath) {
+        SortedMap<Long, Collection<ResolvedSenMLRecord>> result = new TreeMap<>(new Comparator<Long>() {
+            @Override
+            public int compare(Long o1, Long o2) {
+                // null at first place
+                if (o1 == null && o2 == null)
+                    return 0;
+                if (o1 == null)
+                    return -1;
+                if (o2 == null)
+                    return 1;
+                return Long.compare(o2, o1);
+            }
+        });
+
+        long currentTimestamp = System.currentTimeMillis();
+        String currentBasename = "";
+        Long currentBasetime = null;
+        for (SenMLRecord record : records) {
+            // Resolved record :
+            // TODO should we move record resolving into SenMLPack class as this is not specific to LWM2M ?
+            // resolve SenML name (see https://tools.ietf.org/html/rfc8428#section-4.5.1)
+            if (record.getBaseName() != null)
+                currentBasename = record.getBaseName();
+
+            String path = record.getName() == null ? currentBasename : currentBasename + record.getName();
+            LwM2mPath resolvedName = new LwM2mPath(path);
+
+            // Validate SenML resolved name (lwm2m node path)
+            if (!resolvedName.isResourceInstance() && !resolvedName.isResource()) {
+                throw new CodecException(
+                        "Invalid path [%s] for resource, it should be a resource or a resource instance path",
+                        resolvedName);
+            }
+
+            if (!resolvedName.startWith(requestPath)) {
+                throw new CodecException("Invalid path [%s] for resource, it should start by %s", resolvedName,
+                        requestPath);
+            }
+
+            // resolve SenML time (https://tools.ietf.org/html/rfc8428#section-4.5.3)
+            Long resolvedTime = null;
+            if (record.getBaseTime() != null)
+                currentBasetime = record.getBaseTime();
+            if (currentBasetime != null || record.getTime() != null) {
+                Long basetime = currentBasetime != null ? currentBasetime : 0l;
+                resolvedTime = record.getTime() != null ? currentBasetime + record.getTime() : basetime;
+
+                // Values less than 268,435,456 (2**28) represent time relative to the current time.
+                // A negative value indicates seconds in the past from roughly "now".
+                // Positive values up to 2**28 indicate seconds in the future from "now".
+                if (resolvedTime < 268_435_456) {
+                    resolvedTime = currentTimestamp + resolvedTime;
+                }
+                // else
+                // Values greater than or equal to 2**28 represent an absolute time relative to the Unix epoch
+                // (1970-01-01T00:00Z in UTC time)
+            }
+            ResolvedSenMLRecord resolvedRecord = new ResolvedSenMLRecord(record, resolvedName, resolvedTime);
+
+            // Get record list for this time-stamp
+            Collection<ResolvedSenMLRecord> recordList = result.get(resolvedRecord.getTime());
+            if (recordList == null) {
+                recordList = new ArrayList<>();
+                result.put(resolvedRecord.getTime(), recordList);
+            }
+            // Add it to the list
+            recordList.add(resolvedRecord);
+        }
+
+        // Ensure there is at least one entry for null timestamp
+        if (result.isEmpty()) {
+            Collection<ResolvedSenMLRecord> emptylist = Collections.emptyList();
+            result.put((Long) null, emptylist);
+        }
+        return result;
+    }
+
+    /**
+     * Group all SenML record by instanceId
+     * 
+     * @return a map (instanceId => collection of SenML Record)
+     */
+    private Map<Integer, Collection<ResolvedSenMLRecord>> groupRecordsByInstanceId(
+            Collection<ResolvedSenMLRecord> records) throws CodecException {
+        Map<Integer, Collection<ResolvedSenMLRecord>> result = new HashMap<>();
+        for (ResolvedSenMLRecord record : records) {
+            // Get SenML records for this instance
+            Collection<ResolvedSenMLRecord> recordForInstance = result.get(record.getFullpath().getObjectInstanceId());
+            if (recordForInstance == null) {
+                recordForInstance = new ArrayList<>();
+                result.put(record.getFullpath().getObjectInstanceId(), recordForInstance);
+            }
+
+            // Add it to the list
+            recordForInstance.add(record);
+        }
+        return result;
+    }
+
+    private Map<Integer, LwM2mResource> extractLwM2mResources(Collection<ResolvedSenMLRecord> records,
+            LwM2mPath requestPath, LwM2mModel model) throws CodecException {
+        if (records == null)
+            return Collections.emptyMap();
+
+        // Extract LWM2M resources from JSON resource list
+        Map<Integer, LwM2mResource> lwM2mResourceMap = new HashMap<>();
+        Map<LwM2mPath, Map<Integer, SenMLRecord>> multiResourceMap = new HashMap<>();
+
+        for (ResolvedSenMLRecord resolvedRecord : records) {
+            // Build resource path
+            LwM2mPath nodePath = resolvedRecord.getFullpath();
+            SenMLRecord record = resolvedRecord.getRecord();
+
+            // handle LWM2M resources
+            if (nodePath.isResourceInstance()) {
+                // Multi-instance resource
+                // Store multi-instance resource values in a map
+                // we will deal with it later
+                LwM2mPath resourcePath = new LwM2mPath(nodePath.getObjectId(), nodePath.getObjectInstanceId(),
+                        nodePath.getResourceId());
+                Map<Integer, SenMLRecord> multiResource = multiResourceMap.get(resourcePath);
+                if (multiResource == null) {
+                    multiResource = new HashMap<>();
+                    multiResourceMap.put(resourcePath, multiResource);
+                }
+                SenMLRecord previousResInstance = multiResource.put(nodePath.getResourceInstanceId(), record);
+                if (previousResInstance != null) {
+                    throw new CodecException(
+                            "2 RESOURCE_INSTANCE nodes (%s,%s) with the same identifier %d for path %s",
+                            previousResInstance, record, nodePath.getResourceInstanceId(), nodePath);
+                }
+            } else if (nodePath.isResource()) {
+                // Single resource
+                Type expectedType = getResourceType(nodePath, model, record);
+                Object resourceValue = parseResourceValue(record.getResourceValue(), expectedType, nodePath);
+                LwM2mResource res = LwM2mSingleResource.newResource(nodePath.getResourceId(), resourceValue,
+                        expectedType);
+                LwM2mResource previousRes = lwM2mResourceMap.put(nodePath.getResourceId(), res);
+                if (previousRes != null) {
+                    throw new CodecException("2 RESOURCE nodes (%s,%s) with the same identifier %d for path %s",
+                            previousRes, res, res.getId(), nodePath);
+                }
+            } else {
+                throw new CodecException(
+                        "Invalid path [%s] for resource, it should be a resource or a resource instance path",
+                        nodePath);
+            }
+        }
+
+        // Handle multiple resource instances.
+        for (Map.Entry<LwM2mPath, Map<Integer, SenMLRecord>> entry : multiResourceMap.entrySet()) {
+            LwM2mPath resourcePath = entry.getKey();
+            Map<Integer, SenMLRecord> entries = entry.getValue();
+
+            if (entries != null && !entries.isEmpty()) {
+                Type expectedType = getResourceType(resourcePath, model, entries.values().iterator().next());
+                Map<Integer, Object> values = new HashMap<>();
+                for (Entry<Integer, SenMLRecord> e : entries.entrySet()) {
+                    Integer resourceInstanceId = e.getKey();
+                    values.put(resourceInstanceId,
+                            parseResourceValue(e.getValue().getResourceValue(), expectedType, resourcePath));
+                }
+                LwM2mResource resource = LwM2mMultipleResource.newResource(resourcePath.getResourceId(), values,
+                        expectedType);
+                LwM2mResource previousRes = lwM2mResourceMap.put(resourcePath.getResourceId(), resource);
+                if (previousRes != null) {
+                    throw new CodecException("2 RESOURCE nodes (%s,%s) with the same identifier %d for path %s",
+                            previousRes, resource, resource.getId(), resourcePath);
+                }
+            }
+        }
+
+        // If we found nothing, we try to create an empty multi-instance resource
+        if (lwM2mResourceMap.isEmpty() && requestPath.isResource()) {
+            ResourceModel resourceModel = model.getResourceModel(requestPath.getObjectId(),
+                    requestPath.getResourceId());
+            // We create it only if this respect the model
+            if (resourceModel == null || resourceModel.multiple) {
+                Type resourceType = getResourceType(requestPath, model, null);
+                lwM2mResourceMap.put(requestPath.getResourceId(), LwM2mMultipleResource
+                        .newResource(requestPath.getResourceId(), new HashMap<Integer, Object>(), resourceType));
+            }
+        }
+
+        return lwM2mResourceMap;
+    }
+
+    private Object parseResourceValue(Object value, Type expectedType, LwM2mPath path) throws CodecException {
+        LOG.trace("Parse SenML JSON value for path {} and expected type {}: {}", path, expectedType, value);
+
+        try {
+            switch (expectedType) {
+            case INTEGER:
+                return numberToLong((Number) value);
+            case UNSIGNED_INTEGER:
+                return numberToULong((Number) value);
+            case BOOLEAN:
+                return value;
+            case FLOAT:
+                return numberToDouble((Number) value);
+            case TIME:
+                return new Date(numberToLong((Number) value) * 1000L);
+            case OPAQUE:
+                // If the Resource data type is opaque the string value
+                // holds the Base64 encoded representation of the Resource
+                return Base64.decodeBase64((String) value);
+            case STRING:
+                return value;
+            case OBJLNK:
+                return ObjectLink.decodeFromString((String) value);
+            default:
+                throw new CodecException("Unsupported type %s for path %s", expectedType, path);
+            }
+        } catch (Exception e) {
+            throw new CodecException(e, "Invalid content [%s] for type %s for path %s", value, expectedType, path);
+        }
+    }
+
+    private Type getResourceType(LwM2mPath rscPath, LwM2mModel model, SenMLRecord record) {
+        // Use model type in priority
+        ResourceModel rscDesc = model.getResourceModel(rscPath.getObjectId(), rscPath.getResourceId());
+        if (rscDesc != null && rscDesc.type != null)
+            return rscDesc.type;
+
+        // Then json type
+        if (record != null) {
+            Type type = record.getType();
+            if (type != null)
+                return type;
+        }
+
+        // Else use String as default
+        LOG.trace("unknown type for resource use string as default: {}", rscPath);
+        return Type.STRING;
+    }
+
+    protected Long numberToLong(Number number) {
+        return NumberUtil.numberToLong(number);
+    }
+
+    protected ULong numberToULong(Number number) {
+        return NumberUtil.numberToULong(number);
+    }
+
+    protected Double numberToDouble(Number number) {
+        // we get the better approximate value, meaning we can get precision loss
+        return number.doubleValue();
+    }
+
+    private static class ResolvedSenMLRecord {
+        private SenMLRecord record;
+        private LwM2mPath fullpath;
+        private Long time;
+
+        public SenMLRecord getRecord() {
+            return record;
+        }
+
+        public LwM2mPath getFullpath() {
+            return fullpath;
+        }
+
+        public Long getTime() {
+            return time;
+        }
+
+        public ResolvedSenMLRecord(SenMLRecord record, LwM2mPath fullpath, Long time) {
+            super();
+            this.record = record;
+            this.fullpath = fullpath;
+            this.time = time;
+        }
+    }
+}
\ No newline at end of file
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
index 5916b66a..b5feeda8 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/senml/LwM2mNodeSenMLJsonEncoder.java
@@ -15,6 +15,7 @@ package org.eclipse.leshan.core.node.codec.senml;
 
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 import java.util.Map.Entry;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
@@ -27,20 +28,29 @@ 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.LwM2mResourceInstance;
+import org.eclipse.leshan.core.node.ObjectLink;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
-import org.eclipse.leshan.core.node.codec.NodeEncoder;
+import org.eclipse.leshan.core.node.codec.TimestampedNodeEncoder;
 import org.eclipse.leshan.core.util.Base64;
 import org.eclipse.leshan.core.util.Validate;
-import org.eclipse.leshan.senml.SenMLJson;
+import org.eclipse.leshan.senml.SenMLEncoder;
+import org.eclipse.leshan.senml.SenMLException;
 import org.eclipse.leshan.senml.SenMLPack;
 import org.eclipse.leshan.senml.SenMLRecord;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-public class LwM2mNodeSenMLJsonEncoder implements NodeEncoder {
+public class LwM2mNodeSenMLJsonEncoder implements TimestampedNodeEncoder {
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeSenMLJsonEncoder.class);
 
+    private final SenMLEncoder encoder;
+
+    public LwM2mNodeSenMLJsonEncoder(SenMLEncoder encoder) {
+        this.encoder = encoder;
+    }
+
     @Override
     public byte[] encode(LwM2mNode node, LwM2mPath path, LwM2mModel model, LwM2mValueConverter converter) {
         Validate.notNull(node);
@@ -56,7 +66,48 @@ public class LwM2mNodeSenMLJsonEncoder implements NodeEncoder {
 
         SenMLPack pack = new SenMLPack();
         pack.setRecords(internalEncoder.records);
-        return SenMLJson.toSenMLJson(pack).getBytes();
+
+        try {
+            return encoder.toSenML(pack);
+        } catch (SenMLException e) {
+            throw new CodecException(e, "Unable to encode node[path:%s] : %s", path, node);
+        }
+    }
+
+    @Override
+    public byte[] encodeTimestampedData(List<TimestampedLwM2mNode> timestampedNodes, LwM2mPath path, LwM2mModel model,
+            LwM2mValueConverter converter) throws CodecException {
+        Validate.notNull(timestampedNodes);
+        Validate.notNull(path);
+        Validate.notNull(model);
+
+        SenMLPack pack = new SenMLPack();
+        for (TimestampedLwM2mNode timestampedLwM2mNode : timestampedNodes) {
+
+            if (timestampedLwM2mNode.getTimestamp() < 268_435_456) {
+                // The smallest absolute Time value that can be expressed (2**28) is 1978-07-04 21:24:16 UTC.
+                // see https://tools.ietf.org/html/rfc8428#section-4.5.3
+                throw new CodecException(
+                        "Unable to encode timestamped node[path:%s] : invalid timestamp %s, timestamp should be greater or equals to 268,435,456",
+                        path, timestampedLwM2mNode.getTimestamp());
+            }
+
+            InternalEncoder internalEncoder = new InternalEncoder();
+            internalEncoder.objectId = path.getObjectId();
+            internalEncoder.model = model;
+            internalEncoder.requestPath = path;
+            internalEncoder.converter = converter;
+            internalEncoder.records = new ArrayList<>();
+            timestampedLwM2mNode.getNode().accept(internalEncoder);
+            internalEncoder.records.get(0).setBaseTime(timestampedLwM2mNode.getTimestamp());
+            pack.addRecords(internalEncoder.records);
+        }
+
+        try {
+            return encoder.toSenML(pack);
+        } catch (SenMLException e) {
+            throw new CodecException(e, "Unable to encode timestamped node[path:%s] : %s", path, timestampedNodes);
+        }
     }
 
     private static class InternalEncoder implements LwM2mNodeVisitor {
@@ -116,11 +167,25 @@ public class LwM2mNodeSenMLJsonEncoder implements NodeEncoder {
         }
 
         @Override
-        public void visit(LwM2mResourceInstance instance) {
-            throw new UnsupportedOperationException("not yet implemented");
+        public void visit(LwM2mResourceInstance resourceInstance) {
+            LOG.trace("Encoding resource instance {} into SenML JSON", resourceInstance);
+            if (!requestPath.isResourceInstance()) {
+                throw new CodecException("Invalid request path %s for resource  instance encoding", requestPath);
+            }
+
+            // get type for this resource
+            ResourceModel rSpec = model.getResourceModel(objectId, requestPath.getResourceId());
+            Type expectedType = rSpec != null ? rSpec.type : resourceInstance.getType();
+
+            // Using request path as base name, and record doesn't have name
+            addSenMLRecord(null, resourceInstance.getType(), expectedType, resourceInstance.getValue());
         }
 
         private void lwM2mResourceToSenMLRecord(String recordName, LwM2mResource resource) {
+            // get type for this resource
+            ResourceModel rSpec = model.getResourceModel(objectId, resource.getId());
+            Type expectedType = rSpec != null ? rSpec.type : resource.getType();
+
             // create resource element
             if (resource.isMultiInstances()) {
                 for (Entry<Integer, LwM2mResourceInstance> entry : resource.getInstances().entrySet()) {
@@ -132,18 +197,15 @@ public class LwM2mNodeSenMLJsonEncoder implements NodeEncoder {
                         resourceInstanceRecordName = recordName + "/" + entry.getKey();
                     }
 
-                    addSenMLRecord(resourceInstanceRecordName, resource, entry.getValue().getValue());
+                    addSenMLRecord(resourceInstanceRecordName, resource.getType(), expectedType,
+                            entry.getValue().getValue());
                 }
             } else {
-                addSenMLRecord(recordName, resource, resource.getValue());
+                addSenMLRecord(recordName, resource.getType(), expectedType, resource.getValue());
             }
         }
 
-        private void addSenMLRecord(String recordName, LwM2mResource resource, Object value) {
-            // get type for this resource
-            ResourceModel rSpec = model.getResourceModel(objectId, resource.getId());
-            Type expectedType = rSpec != null ? rSpec.type : resource.getType();
-
+        private void addSenMLRecord(String recordName, Type valueType, Type expectedType, Object value) {
             // Create SenML record
             SenMLRecord record = new SenMLRecord();
 
@@ -161,7 +223,7 @@ public class LwM2mNodeSenMLJsonEncoder implements NodeEncoder {
 
             // Convert value using expected type
             LwM2mPath lwM2mResourcePath = new LwM2mPath(bn + n);
-            Object convertedValue = converter.convertValue(value, resource.getType(), expectedType, lwM2mResourcePath);
+            Object convertedValue = converter.convertValue(value, valueType, expectedType, lwM2mResourcePath);
             setResourceValue(convertedValue, expectedType, lwM2mResourcePath, record);
 
             // Add record to the List
@@ -170,11 +232,18 @@ public class LwM2mNodeSenMLJsonEncoder implements NodeEncoder {
 
         private void setResourceValue(Object value, Type type, LwM2mPath resourcePath, SenMLRecord record) {
             LOG.trace("Encoding resource value {} in SenML JSON", value);
+
+            if (type == null || type == Type.NONE) {
+                throw new CodecException(
+                        "Unable to encode value for resource {} without type(probably a executable one)", resourcePath);
+            }
+
             switch (type) {
             case STRING:
                 record.setStringValue((String) value);
                 break;
             case INTEGER:
+            case UNSIGNED_INTEGER:
             case FLOAT:
                 record.setFloatValue((Number) value);
                 break;
@@ -188,7 +257,13 @@ public class LwM2mNodeSenMLJsonEncoder implements NodeEncoder {
                 record.setStringValue(Base64.encodeBase64String((byte[]) value));
                 break;
             case OBJLNK:
-                record.setStringValue(value.toString());
+                try {
+                    record.setStringValue(((ObjectLink) value).encodeToString());
+                } catch (IllegalArgumentException e) {
+                    throw new CodecException(e, "Invalid value [%s] for objectLink resource [%s] ", value,
+                            resourcePath);
+                }
+                break;
             default:
                 throw new CodecException("Invalid value type %s for %s", type, resourcePath);
             }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java
index c423c3a2..4e422862 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextDecoder.java
@@ -16,7 +16,6 @@
 package org.eclipse.leshan.core.node.codec.text;
 
 import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
 import java.util.Date;
 
 import org.eclipse.leshan.core.model.LwM2mModel;
@@ -107,15 +106,10 @@ public class LwM2mNodeTextDecoder implements NodeDecoder {
                 throw new CodecException("Invalid value [%s] for date resource [%s]", value, path);
             }
         case OBJLNK:
-            String[] intArr = value.split(":");
-            if (intArr.length != 2)
-                throw new CodecException("Invalid value %s for objectLink resource [%s]", Arrays.toString(intArr),
-                        path);
             try {
-                return new ObjectLink(Integer.parseInt(intArr[0]), Integer.parseInt(intArr[1]));
-            } catch (NumberFormatException e) {
-                throw new CodecException("Invalid value %s for objectLink resource [%s] ", Arrays.toString(intArr),
-                        path);
+                return ObjectLink.decodeFromString(value);
+            } catch (IllegalArgumentException e) {
+                throw new CodecException(e, "Invalid value [%s] for objectLink resource [%s] ", value, path);
             }
         case OPAQUE:
             if (!Base64.isBase64(value)) {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java
index 44e847ab..7b551373 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/text/LwM2mNodeTextEncoder.java
@@ -132,7 +132,7 @@ public class LwM2mNodeTextEncoder implements NodeEncoder {
                 break;
             case OBJLNK:
                 ObjectLink objlnk = (ObjectLink) val;
-                strValue = String.valueOf(objlnk.getObjectId() + ":" + objlnk.getObjectInstanceId());
+                strValue = objlnk.encodeToString();
                 break;
             case OPAQUE:
                 byte[] binaryValue = (byte[]) val;
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 ac2a3785..85945dfd 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
@@ -41,7 +41,8 @@ public class ContentFormat {
 
     public static final ContentFormat DEFAULT = TLV;
 
-    private static final ContentFormat knownContentFormat[] = new ContentFormat[] { TLV, JSON, TEXT, OPAQUE, LINK };
+    private static final ContentFormat knownContentFormat[] = new ContentFormat[] { TLV, JSON, SENML_JSON, TEXT, OPAQUE,
+                            LINK };
 
     private final String name;
     private final String mediaType;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CreateRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CreateRequest.java
index ad914f7a..a6d31d19 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CreateRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CreateRequest.java
@@ -222,9 +222,10 @@ public class CreateRequest extends AbstractDownlinkRequest<CreateResponse> {
         }
         this.contentFormat = format != null ? format : ContentFormat.TLV; // default to TLV
 
-        if (this.contentFormat == ContentFormat.JSON && unknownObjectInstanceId()) {
+        if (this.contentFormat != ContentFormat.TLV && unknownObjectInstanceId()) {
             throw new InvalidRequestException(
-                    "Missing object instance id for CREATE request (%s) using JSON content format.", target);
+                    "Missing object instance id for CREATE request (%s) using %s content format.", target,
+                    this.contentFormat);
         }
     }
 
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLException.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLException.java
new file mode 100644
index 00000000..13ab6844
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLException.java
@@ -0,0 +1,42 @@
+/*******************************************************************************
+ * 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 v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *     Gemalto M2M GmbH
+ *******************************************************************************/
+package org.eclipse.leshan.senml;
+
+/**
+ * Exception thrown in case of SenML parsing error
+ */
+public class SenMLException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public SenMLException(String message) {
+        super(message);
+    }
+
+    public SenMLException(String message, Object... args) {
+        super(String.format(message, args));
+    }
+
+    public SenMLException(Exception e, String message, Object... args) {
+        super(String.format(message, args), e);
+    }
+
+    public SenMLException(String message, Exception cause) {
+        super(message, cause);
+    }
+
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJson.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJson.java
deleted file mode 100644
index 1170b02a..00000000
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJson.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*******************************************************************************
- * 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:
- *     Boya Zhang - initial API and implementation
- *******************************************************************************/
-
-package org.eclipse.leshan.senml;
-
-import com.eclipsesource.json.Json;
-
-/**
- * Helper for encoding/decoding SenML JSON format
- */
-public class SenMLJson {
-    private static final SenMLJsonPackSerDes serDes = new SenMLJsonPackSerDes();
-
-    public static String toSenMLJson(SenMLPack pack) {
-        return serDes.serializeToJson(pack);
-    }
-
-    public static SenMLPack fromSenMLJson(String jsonString) {
-        return serDes.deserializeFromJson(Json.parse(jsonString).asArray());
-    }
-}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLPack.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLPack.java
index 9d9e6c9a..eba39ec9 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLPack.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLPack.java
@@ -19,12 +19,21 @@ import java.util.Collections;
 import java.util.List;
 
 /**
- * The class representing the SenML Pack of LwM2M
+ * The class representing the SenML Pack.
+ * 
+ * @see <a href="https://tools.ietf.org/html/rfc8428#section-4">rfc8428 - Sensor Measurement Lists (SenML)</a>
  */
 public class SenMLPack {
 
     private List<SenMLRecord> records;
 
+    public SenMLPack() {
+    }
+
+    public SenMLPack(List<SenMLRecord> records) {
+        this.records = records;
+    }
+
     public void addRecord(SenMLRecord record) {
         if (records == null) {
             records = new ArrayList<>();
@@ -33,6 +42,13 @@ public class SenMLPack {
         records.add(record);
     }
 
+    public void addRecords(List<SenMLRecord> records) {
+        if (this.records == null) {
+            this.records = new ArrayList<>();
+        }
+        this.records.addAll(records);
+    }
+
     public void setRecords(List<SenMLRecord> records) {
         this.records = records;
     }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLPackSerDes.java
deleted file mode 100644
index 775c0039..00000000
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLPackSerDes.java
+++ /dev/null
@@ -1,129 +0,0 @@
-/*******************************************************************************
- * 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:
- *     Boya Zhang - initial API and implementation
- *******************************************************************************/
-
-package org.eclipse.leshan.senml;
-
-import org.eclipse.leshan.core.model.ResourceModel.Type;
-import org.eclipse.leshan.core.util.Base64;
-
-import com.eclipsesource.json.JsonArray;
-import com.eclipsesource.json.JsonObject;
-import com.eclipsesource.json.JsonValue;
-
-public class SenMLPackSerDes {
-
-    public String serialize(SenMLPack pack) {
-        JsonArray jsonArray = new JsonArray();
-
-        for (SenMLRecord record : pack.getRecords()) {
-            JsonObject jsonObj = new JsonObject();
-
-            if (record.getBaseName() != null && record.getBaseName().length() > 0) {
-                jsonObj.add("bn", record.getBaseName());
-            }
-
-            if (record.getBaseTime() != null) {
-                jsonObj.add("bt", record.getBaseTime());
-            }
-
-            if (record.getName() != null && record.getName().length() > 0) {
-                jsonObj.add("n", record.getName());
-            }
-
-            if (record.getTime() != null) {
-                jsonObj.add("t", record.getTime());
-            }
-
-            Type type = record.getType();
-            if (type != null) {
-                switch (record.getType()) {
-                case FLOAT:
-                    jsonObj.add("v", record.getFloatValue().floatValue());
-                    break;
-                case BOOLEAN:
-                    jsonObj.add("vb", record.getBooleanValue());
-                    break;
-                case OBJLNK:
-                    jsonObj.add("vlo", record.getObjectLinkValue());
-                    break;
-                case OPAQUE:
-                    jsonObj.add("vd", Base64.encodeBase64String(record.getOpaqueValue()));
-                case STRING:
-                    jsonObj.add("vs", record.getStringValue());
-                    break;
-                default:
-                    break;
-                }
-            }
-
-            jsonArray.add(jsonObj);
-        }
-
-        return jsonArray.toString();
-    }
-
-    public SenMLPack deserialize(JsonArray array) {
-
-        if (array == null)
-            return null;
-
-        SenMLPack pack = new SenMLPack();
-
-        for (JsonValue value : array.values()) {
-            JsonObject o = value.asObject();
-
-            SenMLRecord record = new SenMLRecord();
-
-            JsonValue bn = o.get("bn");
-            if (bn != null && bn.isString())
-                record.setBaseName(bn.asString());
-
-            JsonValue bt = o.get("bt");
-            if (bt != null && bt.isNumber())
-                record.setBaseTime(bt.asLong());
-
-            JsonValue n = o.get("n");
-            if (n != null && n.isString())
-                record.setName(n.asString());
-
-            JsonValue t = o.get("t");
-            if (t != null && t.isNumber())
-                record.setTime(t.asLong());
-
-            JsonValue v = o.get("v");
-            if (v != null && v.isNumber())
-                record.setFloatValue(v.asFloat());
-
-            JsonValue vb = o.get("vb");
-            if (vb != null && vb.isBoolean())
-                record.setBooleanValue(vb.asBoolean());
-
-            JsonValue vs = o.get("vs");
-            if (vs != null && vs.isString())
-                record.setStringValue(vs.asString());
-
-            JsonValue vlo = o.get("vlo");
-            if (vlo != null && vlo.isString())
-                record.setObjectLinkValue(vlo.asString());
-
-            JsonValue vd = o.get("vd");
-            if (vd != null && vd.isString())
-                record.setOpaqueValue(Base64.decodeBase64(vd.asString()));
-
-            pack.addRecord(record);
-        }
-
-        return pack;
-    }
-}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java
index 8ec4db54..51d3d092 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLRecord.java
@@ -17,6 +17,11 @@ package org.eclipse.leshan.senml;
 import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
 
+/**
+ * The class representing the SenML Record.
+ * 
+ * @see <a href="https://tools.ietf.org/html/rfc8428#section-4">rfc8428 - Sensor Measurement Lists (SenML)</a>
+ */
 public class SenMLRecord {
 
     private String baseName = null;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCbor.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCbor.java
similarity index 92%
rename from leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCbor.java
rename to leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCbor.java
index 6d483a19..85020c29 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCbor.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCbor.java
@@ -12,7 +12,9 @@
  *     Boya Zhang - initial API and implementation
  *******************************************************************************/
 
-package org.eclipse.leshan.senml;
+package org.eclipse.leshan.senml.cbor;
+
+import org.eclipse.leshan.senml.SenMLPack;
 
 /**
  * Helper for encoding/decoding SenML CBOR format
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborException.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborException.java
similarity index 96%
rename from leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborException.java
rename to leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborException.java
index a5ecc9af..229d0b06 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborException.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborException.java
@@ -12,7 +12,7 @@
  *     Boya Zhang - initial API and implementation
  *******************************************************************************/
 
-package org.eclipse.leshan.senml;
+package org.eclipse.leshan.senml.cbor;
 
 /**
  * Exception thrown in case of Cbor encoding error
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborPackSerDes.java
similarity index 97%
rename from leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborPackSerDes.java
rename to leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborPackSerDes.java
index 6c7d7538..7483d4b5 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLCborPackSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/SenMLCborPackSerDes.java
@@ -12,11 +12,13 @@
  *     Boya Zhang - initial API and implementation
  *******************************************************************************/
 
-package org.eclipse.leshan.senml;
+package org.eclipse.leshan.senml.cbor;
 
 import java.io.ByteArrayOutputStream;
 
 import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.senml.SenMLPack;
+import org.eclipse.leshan.senml.SenMLRecord;
 
 import com.fasterxml.jackson.dataformat.cbor.CBORFactory;
 import com.fasterxml.jackson.dataformat.cbor.CBORGenerator;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonJacksonEncoderDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonJacksonEncoderDecoder.java
new file mode 100644
index 00000000..54a6dd7d
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonJacksonEncoderDecoder.java
@@ -0,0 +1,60 @@
+/*******************************************************************************
+ * 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:
+ *     Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml.json.jackson;
+
+import java.io.IOException;
+
+import org.eclipse.leshan.core.util.json.JsonException;
+import org.eclipse.leshan.senml.SenMLDecoder;
+import org.eclipse.leshan.senml.SenMLEncoder;
+import org.eclipse.leshan.senml.SenMLException;
+import org.eclipse.leshan.senml.SenMLPack;
+
+import com.eclipsesource.json.ParseException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * Helper for encoding/decoding SenML JSON using Jackson
+ */
+public class SenMLJsonJacksonEncoderDecoder implements SenMLDecoder, SenMLEncoder {
+    private static final SenMLJsonRecordSerDes serDes = new SenMLJsonRecordSerDes();
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    @Override
+    public byte[] toSenML(SenMLPack pack) throws SenMLException {
+        if (pack == null)
+            return null;
+        try {
+            return serDes.bSerialize(pack.getRecords());
+        } catch (JsonException e) {
+            throw new SenMLException("Unable to serialize SenML JSON.", e);
+        }
+    }
+
+    @Override
+    public SenMLPack fromSenML(byte[] jsonString) throws SenMLException {
+        try {
+            JsonNode node = mapper.readTree(jsonString);
+            if (!node.isArray()) {
+                throw new SenMLException("Unable to parse SenML JSON: JsonArray expected but was %s",
+                        node.getNodeType());
+            }
+            return new SenMLPack(serDes.deserialize(node.iterator()));
+        } catch (JsonException | ParseException | IOException e) {
+            throw new SenMLException("Unable to parse SenML JSON.", e);
+        }
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java
new file mode 100644
index 00000000..95bbc54e
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/jackson/SenMLJsonRecordSerDes.java
@@ -0,0 +1,160 @@
+/*******************************************************************************
+ * 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:
+ *     Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml.json.jackson;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.util.Base64;
+import org.eclipse.leshan.core.util.datatype.ULong;
+import org.eclipse.leshan.core.util.json.JacksonJsonSerDes;
+import org.eclipse.leshan.core.util.json.JsonException;
+import org.eclipse.leshan.senml.SenMLRecord;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+public class SenMLJsonRecordSerDes extends JacksonJsonSerDes<SenMLRecord> {
+
+    @Override
+    public JsonNode jSerialize(SenMLRecord record) throws JsonException {
+        ObjectNode jsonObj = JsonNodeFactory.instance.objectNode();
+
+        if (record.getBaseName() != null && record.getBaseName().length() > 0) {
+            jsonObj.put("bn", record.getBaseName());
+        }
+
+        if (record.getBaseTime() != null) {
+            jsonObj.put("bt", record.getBaseTime());
+        }
+
+        if (record.getName() != null && record.getName().length() > 0) {
+            jsonObj.put("n", record.getName());
+        }
+
+        if (record.getTime() != null) {
+            jsonObj.put("t", record.getTime());
+        }
+
+        Type type = record.getType();
+        if (type != null) {
+            switch (record.getType()) {
+            case FLOAT:
+                Number value = record.getFloatValue();
+                // integer
+                if (value instanceof Byte) {
+                    jsonObj.put("v", value.byteValue());
+                } else if (value instanceof Short) {
+                    jsonObj.put("v", value.shortValue());
+                } else if (value instanceof Integer) {
+                    jsonObj.put("v", value.intValue());
+                } else if (value instanceof Long) {
+                    jsonObj.put("v", value.longValue());
+                } else if (value instanceof BigInteger) {
+                    jsonObj.put("v", (BigInteger) value);
+                }
+                // unsigned integer
+                else if (value instanceof ULong) {
+                    jsonObj.put("v", ((ULong) value).toBigInteger());
+                }
+                // floating-point
+                else if (value instanceof Float) {
+                    jsonObj.put("v", value.floatValue());
+                } else if (value instanceof Double) {
+                    jsonObj.put("v", value.doubleValue());
+                } else if (value instanceof BigDecimal) {
+                    jsonObj.put("v", (BigDecimal) value);
+                }
+                break;
+            case BOOLEAN:
+                jsonObj.put("vb", record.getBooleanValue());
+                break;
+            case OBJLNK:
+                jsonObj.put("vlo", record.getObjectLinkValue());
+                break;
+            case OPAQUE:
+                jsonObj.put("vd", Base64.encodeBase64String(record.getOpaqueValue()));
+            case STRING:
+                jsonObj.put("vs", record.getStringValue());
+                break;
+            default:
+                break;
+            }
+        }
+        return jsonObj;
+    }
+
+    @Override
+    public SenMLRecord deserialize(JsonNode o) throws JsonException {
+        if (o == null)
+            return null;
+
+        SenMLRecord record = new SenMLRecord();
+
+        JsonNode bn = o.get("bn");
+        if (bn != null && bn.isTextual())
+            record.setBaseName(bn.asText());
+
+        JsonNode bt = o.get("bt");
+        if (bt != null && bt.isNumber())
+            record.setBaseTime(bt.asLong());
+
+        JsonNode n = o.get("n");
+        if (n != null && n.isTextual())
+            record.setName(n.asText());
+
+        JsonNode t = o.get("t");
+        if (t != null && t.isNumber())
+            record.setTime(t.asLong());
+
+        JsonNode v = o.get("v");
+        boolean hasValue = false;
+        if (v != null && v.isNumber()) {
+            record.setFloatValue(v.numberValue());
+            hasValue = true;
+        }
+
+        JsonNode vb = o.get("vb");
+        if (vb != null && vb.isBoolean()) {
+            record.setBooleanValue(vb.asBoolean());
+            hasValue = true;
+        }
+
+        JsonNode vs = o.get("vs");
+        if (vs != null && vs.isTextual()) {
+            record.setStringValue(vs.asText());
+            hasValue = true;
+        }
+
+        JsonNode vlo = o.get("vlo");
+        if (vlo != null && vlo.isTextual()) {
+            record.setObjectLinkValue(vlo.asText());
+            hasValue = true;
+        }
+
+        JsonNode vd = o.get("vd");
+        if (vd != null && vd.isTextual()) {
+            record.setOpaqueValue(Base64.decodeBase64(vd.asText()));
+            hasValue = true;
+        }
+
+        if (!hasValue)
+            throw new JsonException("Invalid SenML record : record must have a value (v,vb,vlo,vd,vs) : %s", o);
+
+        return record;
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/json/minimaljson/SenMLJsonMinimalEncoderDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/minimaljson/SenMLJsonMinimalEncoderDecoder.java
new file mode 100644
index 00000000..ee3134b2
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/minimaljson/SenMLJsonMinimalEncoderDecoder.java
@@ -0,0 +1,49 @@
+/*******************************************************************************
+ * 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:
+ *     Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml.json.minimaljson;
+
+import org.eclipse.leshan.core.util.json.JsonException;
+import org.eclipse.leshan.senml.SenMLDecoder;
+import org.eclipse.leshan.senml.SenMLEncoder;
+import org.eclipse.leshan.senml.SenMLException;
+import org.eclipse.leshan.senml.SenMLPack;
+
+import com.eclipsesource.json.Json;
+import com.eclipsesource.json.ParseException;
+
+/**
+ * Helper for encoding/decoding SenML JSON using minimal-json
+ */
+public class SenMLJsonMinimalEncoderDecoder implements SenMLDecoder, SenMLEncoder {
+    private static final SenMLJsonPackSerDes serDes = new SenMLJsonPackSerDes();
+
+    @Override
+    public byte[] toSenML(SenMLPack pack) throws SenMLException {
+        try {
+            return serDes.serializeToJson(pack);
+        } catch (JsonException e) {
+            throw new SenMLException("Unable to serialize LWM2M JSON.", e);
+        }
+    }
+
+    @Override
+    public SenMLPack fromSenML(byte[] jsonString) throws SenMLException {
+        try {
+            return serDes.deserializeFromJson(Json.parse(new String(jsonString)).asArray());
+        } catch (JsonException | ParseException e) {
+            throw new SenMLException("Unable to parse SenML JSON.", e);
+        }
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJsonPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/minimaljson/SenMLJsonPackSerDes.java
similarity index 91%
rename from leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJsonPackSerDes.java
rename to leshan-core/src/main/java/org/eclipse/leshan/senml/json/minimaljson/SenMLJsonPackSerDes.java
index 55583c1c..f5068bb3 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLJsonPackSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/json/minimaljson/SenMLJsonPackSerDes.java
@@ -12,10 +12,12 @@
  *     Boya Zhang - initial API and implementation
  *******************************************************************************/
 
-package org.eclipse.leshan.senml;
+package org.eclipse.leshan.senml.json.minimaljson;
 
 import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.util.Base64;
+import org.eclipse.leshan.senml.SenMLPack;
+import org.eclipse.leshan.senml.SenMLRecord;
 
 import com.eclipsesource.json.JsonArray;
 import com.eclipsesource.json.JsonObject;
@@ -23,7 +25,7 @@ import com.eclipsesource.json.JsonValue;
 
 public class SenMLJsonPackSerDes {
 
-    public String serializeToJson(SenMLPack pack) {
+    public byte[] serializeToJson(SenMLPack pack) {
         JsonArray jsonArray = new JsonArray();
 
         for (SenMLRecord record : pack.getRecords()) {
@@ -49,7 +51,7 @@ public class SenMLJsonPackSerDes {
             if (type != null) {
                 switch (record.getType()) {
                 case FLOAT:
-                    jsonObj.add("v", record.getFloatValue().floatValue());
+                    jsonObj.add("v", record.getFloatValue().doubleValue());
                     break;
                 case BOOLEAN:
                     jsonObj.add("vb", record.getBooleanValue());
@@ -70,7 +72,7 @@ public class SenMLJsonPackSerDes {
             jsonArray.add(jsonObj);
         }
 
-        return jsonArray.toString();
+        return jsonArray.toString().getBytes();
     }
 
     public SenMLPack deserializeFromJson(JsonArray array) {
@@ -103,7 +105,7 @@ public class SenMLJsonPackSerDes {
 
             JsonValue v = o.get("v");
             if (v != null && v.isNumber())
-                record.setFloatValue(v.asFloat());
+                record.setFloatValue(v.asDouble());
 
             JsonValue vb = o.get("vb");
             if (vb != null && vb.isBoolean())
diff --git a/leshan-server-demo/src/main/resources/webapp/partials/client-detail.html b/leshan-server-demo/src/main/resources/webapp/partials/client-detail.html
index 6f2ed0d1..af5cab9f 100644
--- a/leshan-server-demo/src/main/resources/webapp/partials/client-detail.html
+++ b/leshan-server-demo/src/main/resources/webapp/partials/client-detail.html
@@ -25,6 +25,7 @@
 				</button>
 				<ul class="dropdown-menu dropdown-menu-contentformat" role="menu">
 					<li><a ng-click="settings.multi.format='TLV'">TLV</a></li>
+					<li><a ng-click="settings.multi.format='SENML_JSON'">SENML_JSON</a></li>
 					<li><a ng-click="settings.multi.format='JSON'">JSON</a></li>
 				</ul>
 			</div>
@@ -35,6 +36,7 @@
 				</button>
 				<ul class="dropdown-menu dropdown-menu-contentformat" role="menu">
 					<li><a ng-click="settings.single.format='TLV'">TLV</a></li>
+					<li><a ng-click="settings.single.format='SENML_JSON'">SENML_JSON</a></li>
 					<li><a ng-click="settings.single.format='JSON'">JSON</a></li>
 					<li><a ng-click="settings.single.format='Text'">Text</a></li>
 					<li><a ng-click="settings.single.format='Opaque'">Opaque</a></li>


Test patch:
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonDeserializerTest.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLDecoder.java
similarity index 53%
rename from leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonDeserializerTest.java
rename to leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLDecoder.java
index 94b9ea1d..9551fd2e 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonDeserializerTest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLDecoder.java
@@ -1,30 +1,26 @@
 /*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
  * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
+ * are made available under the terms of the Eclipse Public License v2.0
  * and Eclipse Distribution License v1.0 which accompany this distribution.
  * 
  * The Eclipse Public License is available at
- *    http://www.eclipse.org/legal/epl-v10.html
+ *    http://www.eclipse.org/legal/epl-v20.html
  * and the Eclipse Distribution License is available at
  *    http://www.eclipse.org/org/documents/edl-v10.html.
  * 
  * Contributors:
- *     Boya Zhang - initial API and implementation
+ *     Sierra Wireless - initial API and implementation
  *******************************************************************************/
-
 package org.eclipse.leshan.senml;
 
-import org.junit.Assert;
-import org.junit.Test;
-
-public class SenMLJsonDeserializerTest extends AbstractSenMLTest {
-
-    @Test
-    public void deserialize_device_object() {
-        String dataString = givenSenMLJsonExample();
-        SenMLPack pack = SenMLJson.fromSenMLJson(dataString);
+/**
+ * A decoder for SenML format.
+ * 
+ * @see <a href="https://tools.ietf.org/html/rfc8428">rfc8428 - Sensor Measurement Lists (SenML)</a>
+ */
+public interface SenMLDecoder {
 
-        String outString = SenMLJson.toSenMLJson(pack);
-        Assert.assertEquals(dataString.trim(), outString);
-    }
+    SenMLPack fromSenML(byte[] encodedSenML) throws SenMLException;
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerializerTest.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLEncoder.java
similarity index 57%
rename from leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerializerTest.java
rename to leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLEncoder.java
index 66c64aac..57c72e85 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerializerTest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/SenMLEncoder.java
@@ -1,27 +1,27 @@
 /*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
  * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the Eclipse Public License v1.0
+ * are made available under the terms of the Eclipse Public License v2.0
  * and Eclipse Distribution License v1.0 which accompany this distribution.
  * 
  * The Eclipse Public License is available at
- *    http://www.eclipse.org/legal/epl-v10.html
+ *    http://www.eclipse.org/legal/epl-v20.html
  * and the Eclipse Distribution License is available at
  *    http://www.eclipse.org/org/documents/edl-v10.html.
  * 
  * Contributors:
- *     Boya Zhang - initial API and implementation
+ *     Sierra Wireless - initial API and implementation
  *******************************************************************************/
-
 package org.eclipse.leshan.senml;
 
-import org.junit.Assert;
-import org.junit.Test;
+/**
+ * An encoder for SenML format.
+ * 
+ * @see <a href="https://tools.ietf.org/html/rfc8428">rfc8428 - Sensor Measurement Lists (SenML)</a>
+ */
+public interface SenMLEncoder {
 
-public class SenMLJsonSerializerTest extends AbstractSenMLTest {
+    public byte[] toSenML(SenMLPack pack) throws SenMLException;
 
-    @Test
-    public void serialize_device_object_to_senml_json() {
-        String json = SenMLJson.toSenMLJson(givenDeviceObjectInstance());
-        Assert.assertTrue(json.equals(givenSenMLJsonExample()));
-    }
 }
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 c1d27b8a..f5492c1f 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
@@ -44,6 +44,7 @@ import org.eclipse.leshan.core.tlv.Tlv;
 import org.eclipse.leshan.core.tlv.Tlv.TlvType;
 import org.eclipse.leshan.core.tlv.TlvEncoder;
 import org.eclipse.leshan.core.util.Hex;
+import org.eclipse.leshan.core.util.datatype.ULong;
 import org.junit.BeforeClass;
 import org.junit.Test;
 
@@ -808,4 +809,213 @@ public class LwM2mNodeDecoderTest {
         assertEquals(11, resource.getId());
         assertTrue(resource.getInstances().size() == 2);
     }
+
+    @Test
+    public void senml_json_decode_acl_object() {
+        StringBuilder payload = new StringBuilder();
+        payload.append("[{\"bn\":\"/2/3/\",\"n\":\"0\",\"v\":3},");
+        payload.append("{\"n\":\"1\",\"v\":1},");
+        payload.append("{\"n\":\"3\",\"v\":123},");
+        payload.append("{\"bn\":\"/2/2/\",\"n\":\"0\",\"v\":4},");
+        payload.append("{\"n\":\"1\",\"v\":2},");
+        payload.append("{\"n\":\"3\",\"v\":124}]");
+
+        LwM2mObject object = decoder.decode(payload.toString().getBytes(), ContentFormat.SENML_JSON,
+                new LwM2mPath("/2"), model, LwM2mObject.class);
+
+        assertNotNull(object);
+        assertEquals(2, object.getId());
+        assertEquals(2, object.getInstances().size());
+        assertEquals(3l, object.getInstance(3).getResource(0).getValue());
+        assertEquals(1l, object.getInstance(3).getResource(1).getValue());
+        assertEquals(123l, object.getInstance(3).getResource(3).getValue());
+        assertEquals(4l, object.getInstance(2).getResource(0).getValue());
+        assertEquals(2l, object.getInstance(2).getResource(1).getValue());
+        assertEquals(124l, object.getInstance(2).getResource(3).getValue());
+
+    }
+
+    @Test
+    public void senml_json_decode_device_object_instance() {
+        StringBuilder payload = new StringBuilder();
+        payload.append("[{\"bn\":\"/3/0/\",\"n\":\"0\",\"vs\":\"Open Mobile Alliance\"},");
+        payload.append("{\"n\":\"1\",\"vs\":\"Lightweight M2M Client\"},");
+        payload.append("{\"n\":\"2\",\"vs\":\"345000123\"},");
+        payload.append("{\"n\":\"3\",\"vs\":\"1.0\"},");
+        payload.append("{\"n\":\"6/0\",\"v\":1},");
+        payload.append("{\"n\":\"6/1\",\"v\":5},");
+        payload.append("{\"n\":\"7/0\",\"v\":3800},");
+        payload.append("{\"n\":\"7/1\",\"v\":5000},");
+        payload.append("{\"n\":\"8/0\",\"v\":125},");
+        payload.append("{\"n\":\"8/1\",\"v\":900},");
+        payload.append("{\"n\":\"9\",\"v\":100},");
+        payload.append("{\"n\":\"10\",\"v\":15},");
+        payload.append("{\"n\":\"11/0\",\"v\":0},");
+        payload.append("{\"n\":\"13\",\"v\":1367491215},");
+        payload.append("{\"n\":\"14\",\"vs\":\"+02:00\"},");
+        payload.append("{\"n\":\"16\",\"vs\":\"U\"}]");
+
+        LwM2mObjectInstance instance = decoder.decode(payload.toString().getBytes(), ContentFormat.SENML_JSON,
+                new LwM2mPath("/3/0"), model, LwM2mObjectInstance.class);
+
+        assertNotNull(instance);
+        assertEquals(0, instance.getId());
+        assertEquals("Open Mobile Alliance", instance.getResource(0).getValue());
+        assertEquals("Lightweight M2M Client", instance.getResource(1).getValue());
+        assertEquals("345000123", instance.getResource(2).getValue());
+        assertEquals("1.0", instance.getResource(3).getValue());
+
+        assertEquals(1l, instance.getResource(6).getValue(0));
+        assertEquals(5l, instance.getResource(6).getValue(1));
+        assertEquals(3800l, instance.getResource(7).getValue(0));
+        assertEquals(5000l, instance.getResource(7).getValue(1));
+        assertEquals(125l, instance.getResource(8).getValue(0));
+        assertEquals(900l, instance.getResource(8).getValue(1));
+
+        assertEquals(100l, instance.getResource(9).getValue());
+        assertEquals(15l, instance.getResource(10).getValue());
+
+        assertEquals(0l, instance.getResource(11).getValue(0));
+
+        Date time = (Date) instance.getResource(13).getValue();
+        assertEquals(1367491215000l, time.getTime());
+
+        assertEquals("+02:00", instance.getResource(14).getValue());
+        assertEquals("U", instance.getResource(16).getValue());
+    }
+
+    @Test
+    public void senml_json_decode_single_resource() {
+        String payload = "[{\"bn\":\"/3/0/0\",\"vs\":\"Open Mobile Alliance\"}]";
+        LwM2mResource resource = decoder.decode(payload.getBytes(), ContentFormat.SENML_JSON, new LwM2mPath("/3/0/0"),
+                model, LwM2mResource.class);
+
+        assertNotNull(resource);
+        assertTrue(!resource.isMultiInstances());
+        assertEquals(0, resource.getId());
+        assertEquals("Open Mobile Alliance", resource.getValue());
+
+        payload = "[{\"n\":\"/6/0/3\",\"v\":20.0}]";
+        resource = decoder.decode(payload.getBytes(), ContentFormat.SENML_JSON, new LwM2mPath("/6/0/3"), model,
+                LwM2mResource.class);
+
+        assertNotNull(resource);
+        assertTrue(!resource.isMultiInstances());
+        assertEquals(3, resource.getId());
+        assertEquals(20.0, resource.getValue());
+    }
+
+    @Test
+    public void senml_json_decode_multiple_resource() {
+        StringBuilder payload = new StringBuilder();
+        payload.append("[{\"bn\":\"/3/0/7/\",\"n\":\"0\",\"v\":3800},");
+        payload.append("{\"n\":\"1\",\"v\":5000}]");
+        LwM2mResource multipleResources = decoder.decode(payload.toString().getBytes(), ContentFormat.SENML_JSON,
+                new LwM2mPath("/3/0/7"), model, LwM2mResource.class);
+
+        assertNotNull(multipleResources);
+        assertTrue(multipleResources.isMultiInstances());
+        assertEquals(3800l, multipleResources.getValue(0));
+        assertEquals(5000l, multipleResources.getValue(1));
+    }
+
+    @Test
+    public void senml_json_decode_ulong() {
+        // 18446744073709551615 can not hold in Long
+        String payload = "[{\"n\":\"/0/0/16\",\"v\":18446744073709551615}]";
+        LwM2mResource resource = decoder.decode(payload.getBytes(), ContentFormat.SENML_JSON, new LwM2mPath("/0/0/16"),
+                model, LwM2mResource.class);
+
+        assertNotNull(resource);
+        assertTrue(!resource.isMultiInstances());
+        assertEquals(16, resource.getId());
+        assertEquals(ULong.valueOf("18446744073709551615"), resource.getValue());
+    }
+
+    @Test
+    public void senml_json_decode_long() {
+        // 9223372036854775800 long value can not hold in double (will be approximate to 9223372036854775808)
+        String payload = "[{\"n\":\"/1/0/2\",\"v\":9223372036854775800}]";
+        LwM2mResource resource = decoder.decode(payload.getBytes(), ContentFormat.SENML_JSON, new LwM2mPath("/1/0/2"),
+                model, LwM2mResource.class);
+
+        assertNotNull(resource);
+        assertTrue(!resource.isMultiInstances());
+        assertEquals(2, resource.getId());
+        assertEquals(9223372036854775800l, resource.getValue());
+    }
+
+    @Test
+    public void senml_timestamped_resources() throws CodecException {
+        // json content for instance 0 of device object
+        StringBuilder b = new StringBuilder();
+        b.append("[{\"bn\":\"/1024/0/1\",\"v\":22.9,\"bt\":268500000},");
+        b.append("{\"v\":22.4,\"t\":-5},");
+        b.append("{\"v\":24.1,\"t\":-50}],");
+
+        List<TimestampedLwM2mNode> timestampedResources = decoder.decodeTimestampedData(b.toString().getBytes(),
+                ContentFormat.SENML_JSON, new LwM2mPath(1024, 0, 1), model);
+
+        assertEquals(3, timestampedResources.size());
+        assertEquals(Long.valueOf(268500000), timestampedResources.get(0).getTimestamp());
+        assertEquals(22.9d, ((LwM2mResource) timestampedResources.get(0).getNode()).getValue());
+        assertEquals(Long.valueOf(268500000 - 5), timestampedResources.get(1).getTimestamp());
+        assertEquals(22.4d, ((LwM2mResource) timestampedResources.get(1).getNode()).getValue());
+        assertEquals(Long.valueOf(268500000 - 50), timestampedResources.get(2).getTimestamp());
+        assertEquals(24.1d, ((LwM2mResource) timestampedResources.get(2).getNode()).getValue());
+    }
+
+    @Test
+    public void senml_timestamped_instances() throws CodecException {
+        // json content for instance 0 of device object
+        StringBuilder b = new StringBuilder();
+        b.append("[{\"bn\":\"/1024/0/\",\"bt\":268600000, \"n\":\"1\", \"v\":22.9},");
+        b.append("{\"n\":\"1\",\"v\":24.1,\"t\":-50},");
+        b.append("{\"bt\":268500000, \"n\":\"0\",\"vs\":\"a string\"},");
+        b.append("{\"n\":\"1\",\"v\":22.4}]");
+
+        List<TimestampedLwM2mNode> timestampedResources = decoder.decodeTimestampedData(b.toString().getBytes(),
+                ContentFormat.SENML_JSON, new LwM2mPath(1024, 0), model);
+
+        assertEquals(3, timestampedResources.size());
+        assertEquals(Long.valueOf(268600000), timestampedResources.get(0).getTimestamp());
+        assertEquals(22.9d, ((LwM2mObjectInstance) timestampedResources.get(0).getNode()).getResource(1).getValue());
+
+        assertEquals(Long.valueOf(268600000 - 50), timestampedResources.get(1).getTimestamp());
+        assertEquals(24.1d, ((LwM2mObjectInstance) timestampedResources.get(1).getNode()).getResource(1).getValue());
+
+        assertEquals(Long.valueOf(268500000), timestampedResources.get(2).getTimestamp());
+        assertEquals(22.4d, ((LwM2mObjectInstance) timestampedResources.get(2).getNode()).getResource(1).getValue());
+        assertEquals("a string",
+                ((LwM2mObjectInstance) timestampedResources.get(2).getNode()).getResource(0).getValue());
+
+    }
+
+    @Test
+    public void senml_timestamped_Object() throws CodecException {
+        // json content for instance 0 of device object
+        StringBuilder b = new StringBuilder();
+        b.append("[{\"bn\":\"/1024/\",\"bt\":268600000,\"n\":\"0/1\",\"v\":22.9},");
+        b.append("{\"n\":\"0/0\",\"vs\":\"a string\",\"t\":-50},");
+        b.append("{\"n\":\"1/1\",\"v\":23,\"t\":-50},");
+        b.append("{\"n\":\"0/1\",\"v\":24.1,\"t\":-5}]");
+
+        List<TimestampedLwM2mNode> timestampedResources = decoder.decodeTimestampedData(b.toString().getBytes(),
+                ContentFormat.SENML_JSON, new LwM2mPath(1024), model);
+
+        assertEquals(3, timestampedResources.size());
+        assertEquals(Long.valueOf(268600000), timestampedResources.get(0).getTimestamp());
+        assertEquals(22.9d,
+                ((LwM2mObject) timestampedResources.get(0).getNode()).getInstance(0).getResource(1).getValue());
+
+        assertEquals(Long.valueOf(268600000 - 5), timestampedResources.get(1).getTimestamp());
+        assertEquals(24.1d,
+                ((LwM2mObject) timestampedResources.get(1).getNode()).getInstance(0).getResource(1).getValue());
+
+        assertEquals(Long.valueOf(268600000 - 50), timestampedResources.get(2).getTimestamp());
+        assertEquals(23.0d,
+                ((LwM2mObject) timestampedResources.get(2).getNode()).getInstance(1).getResource(1).getValue());
+        assertEquals("a string",
+                ((LwM2mObject) timestampedResources.get(2).getNode()).getInstance(0).getResource(0).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 2f98bda7..d32629ca 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
@@ -319,7 +319,7 @@ public class LwM2mNodeEncoderTest {
         b.append("{\"n\":\"9\",\"v\":100},");
         b.append("{\"n\":\"10\",\"v\":15},");
         b.append("{\"n\":\"11/0\",\"v\":0},");
-        b.append("{\"n\":\"13\",\"v\":1.3674912E9},");
+        b.append("{\"n\":\"13\",\"v\":1367491215},");
         b.append("{\"n\":\"14\",\"vs\":\"+02:00\"},");
         b.append("{\"n\":\"16\",\"vs\":\"U\"}]");
 
@@ -347,4 +347,23 @@ public class LwM2mNodeEncoderTest {
         String expected = "[{\"bn\":\"/3/0/7/\",\"n\":\"0\",\"v\":3800},{\"n\":\"1\",\"v\":5000}]";
         Assert.assertEquals(expected, new String(encoded));
     }
+
+    @Test
+    public void senml_encode_timestamped_resources() throws CodecException {
+        List<TimestampedLwM2mNode> data = new ArrayList<>();
+        data.add(new TimestampedLwM2mNode(268_500_000L, LwM2mSingleResource.newFloatResource(1, 22.9)));
+        data.add(new TimestampedLwM2mNode(268_500_010L, LwM2mSingleResource.newFloatResource(1, 22.4)));
+        data.add(new TimestampedLwM2mNode(268_500_020L, LwM2mSingleResource.newFloatResource(1, 24.1)));
+
+        byte[] encoded = encoder.encodeTimestampedData(data, ContentFormat.SENML_JSON, new LwM2mPath(1024, 0, 1),
+                model);
+
+        StringBuilder b = new StringBuilder();
+        b.append("[{\"bn\":\"/1024/0/1\",\"bt\":268500000,\"v\":22.9},");
+        b.append("{\"bn\":\"/1024/0/1\",\"bt\":268500010,\"v\":22.4},");
+        b.append("{\"bn\":\"/1024/0/1\",\"bt\":268500020,\"v\":24.1}]");
+
+        String expected = b.toString();
+        Assert.assertEquals(expected, new String(encoded));
+    }
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLCborSerializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLCborSerializerTest.java
index 22b8d526..90878d26 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLCborSerializerTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLCborSerializerTest.java
@@ -15,6 +15,7 @@
 package org.eclipse.leshan.senml;
 
 import org.eclipse.leshan.core.util.Hex;
+import org.eclipse.leshan.senml.cbor.SenMLCbor;
 import org.junit.Assert;
 import org.junit.Test;
 
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerDesTest.java b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerDesTest.java
new file mode 100644
index 00000000..33319d62
--- /dev/null
+++ b/leshan-core/src/test/java/org/eclipse/leshan/senml/SenMLJsonSerDesTest.java
@@ -0,0 +1,55 @@
+/*******************************************************************************
+ * 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:
+ *     Boya Zhang - initial API and implementation
+ *******************************************************************************/
+
+package org.eclipse.leshan.senml;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder;
+import org.eclipse.leshan.senml.json.minimaljson.SenMLJsonMinimalEncoderDecoder;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class SenMLJsonSerDesTest extends AbstractSenMLTest {
+
+    @Parameterized.Parameters(name = "{2}")
+    public static Collection<?> senMLJsonencoderDecoder() {
+        SenMLJsonMinimalEncoderDecoder minimal = new SenMLJsonMinimalEncoderDecoder();
+        SenMLJsonJacksonEncoderDecoder jackson = new SenMLJsonJacksonEncoderDecoder();
+        return Arrays.asList(new Object[][] { //
+                                { minimal, minimal, "minimal-json" }, //
+                                { jackson, jackson, "jackson" } });
+    }
+
+    private SenMLEncoder encoder;
+    private SenMLDecoder decoder;
+
+    public SenMLJsonSerDesTest(SenMLEncoder encoder, SenMLDecoder decoder, String encoderDecoderName) {
+        this.encoder = encoder;
+        this.decoder = decoder;
+    }
+
+    @Test
+    public void deserialize_device_object_with_minimalJson() throws SenMLException {
+        String dataString = givenSenMLJsonExample();
+        SenMLPack pack = decoder.fromSenML(dataString.getBytes());
+        String outString = new String(encoder.toSenML(pack));
+        Assert.assertEquals(dataString.trim(), outString);
+    }
+
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java
index 7280f22b..abdc2881 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/IntegrationTestHelper.java
@@ -26,6 +26,8 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.TimeoutException;
 import java.util.concurrent.atomic.AtomicReference;
 
+import org.eclipse.californium.core.network.CoapEndpoint;
+import org.eclipse.californium.elements.Connector;
 import org.eclipse.leshan.client.californium.LeshanClient;
 import org.eclipse.leshan.client.californium.LeshanClientBuilder;
 import org.eclipse.leshan.client.object.Device;
@@ -60,7 +62,7 @@ import org.eclipse.leshan.server.security.InMemorySecurityStore;
 public class IntegrationTestHelper {
     public static final Random r = new Random();
 
-    static final String MODEL_NUMBER = "IT-TEST-123";
+    public static final String MODEL_NUMBER = "IT-TEST-123";
     public static final long LIFETIME = 2;
 
     public static final int TEST_OBJECT_ID = 2000;
@@ -79,9 +81,9 @@ public class IntegrationTestHelper {
 
     public static final String MULTI_INSTANCE = "multiinstance";
 
-    LeshanServer server;
-    LeshanClient client;
-    AtomicReference<String> currentEndpointIdentifier = new AtomicReference<String>();
+    public LeshanServer server;
+    public LeshanClient client;
+    public AtomicReference<String> currentEndpointIdentifier = new AtomicReference<String>();
 
     private SynchronousClientObserver clientObserver = new SynchronousClientObserver();
     private SynchronousRegistrationListener registrationListener = new SynchronousRegistrationListener() {
@@ -91,7 +93,7 @@ public class IntegrationTestHelper {
         }
     };
 
-    protected List<ObjectModel> createObjectModels() {
+    public List<ObjectModel> createObjectModels() {
         // load default object from the spec
         List<ObjectModel> objectModels = ObjectLoader.loadDefault();
         // define custom model for testing purpose
@@ -329,4 +331,9 @@ public class IntegrationTestHelper {
     public Registration getLastRegistration() {
         return registrationListener.getLastRegistration();
     }
+
+    public Connector getClientConnector(ServerIdentity server) {
+        CoapEndpoint endpoint = (CoapEndpoint) client.coap().getServer().getEndpoint(client.getAddress(server));
+        return endpoint.getConnector();
+    }
 }
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ReadTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ReadTest.java
deleted file mode 100644
index bdeed11d..00000000
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ReadTest.java
+++ /dev/null
@@ -1,253 +0,0 @@
-/*******************************************************************************
- * 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 v2.0
- * and Eclipse Distribution License v1.0 which accompany this distribution.
- * 
- * The Eclipse Public License is available at
- *    http://www.eclipse.org/legal/epl-v20.html
- * and the Eclipse Distribution License is available at
- *    http://www.eclipse.org/org/documents/edl-v10.html.
- * 
- * Contributors:
- *     Zebra Technologies - initial API and implementation
- *     Achim Kraus (Bosch Software Innovations GmbH) - add test for read security object
- *     Achim Kraus (Bosch Software Innovations GmbH) - replace close() with destroy()
- *******************************************************************************/
-package org.eclipse.leshan.integration.tests;
-
-import static org.eclipse.leshan.core.ResponseCode.*;
-import static org.eclipse.leshan.integration.tests.IntegrationTestHelper.*;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.Is.is;
-import static org.hamcrest.core.IsInstanceOf.instanceOf;
-import static org.junit.Assert.*;
-
-import org.eclipse.californium.core.coap.Response;
-import org.eclipse.leshan.core.model.ResourceModel.Type;
-import org.eclipse.leshan.core.node.LwM2mObject;
-import org.eclipse.leshan.core.node.LwM2mObjectInstance;
-import org.eclipse.leshan.core.node.LwM2mResource;
-import org.eclipse.leshan.core.node.LwM2mResourceInstance;
-import org.eclipse.leshan.core.request.ContentFormat;
-import org.eclipse.leshan.core.request.ReadRequest;
-import org.eclipse.leshan.core.response.ReadResponse;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class ReadTest {
-
-    public IntegrationTestHelper helper = new IntegrationTestHelper();
-
-    @Before
-    public void start() {
-        helper.initialize();
-        helper.createServer();
-        helper.server.start();
-        helper.createClient();
-        helper.client.start();
-        helper.waitForRegistrationAtServerSide(1);
-    }
-
-    @After
-    public void stop() {
-        helper.client.destroy(false);
-        helper.server.destroy();
-        helper.dispose();
-    }
-
-    @Test
-    public void can_read_empty_object() throws InterruptedException {
-        // read ACL object
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(2));
-
-        // verify result
-        assertEquals(CONTENT, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        LwM2mObject object = (LwM2mObject) response.getContent();
-        assertEquals(2, object.getId());
-        assertTrue(object.getInstances().isEmpty());
-    }
-
-    @Test
-    public void can_read_object() throws InterruptedException {
-        // read device object
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3));
-
-        // verify result
-        assertEquals(CONTENT, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        LwM2mObject object = (LwM2mObject) response.getContent();
-        assertEquals(3, object.getId());
-
-        LwM2mObjectInstance instance = object.getInstance(0);
-        assertEquals(0, instance.getId());
-    }
-
-    @Test
-    public void can_read_object_instance() throws InterruptedException {
-        // read device single instance
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 0));
-
-        // verify result
-        assertEquals(CONTENT, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        LwM2mObjectInstance instance = (LwM2mObjectInstance) response.getContent();
-        assertEquals(0, instance.getId());
-    }
-
-    @Test
-    public void can_read_resource() throws InterruptedException {
-        // read device model number
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 0, 1));
-
-        // verify result
-        assertEquals(CONTENT, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        LwM2mResource resource = (LwM2mResource) response.getContent();
-        assertEquals(1, resource.getId());
-        assertEquals(IntegrationTestHelper.MODEL_NUMBER, resource.getValue());
-    }
-
-    @Test
-    public void can_read_resource_instance() throws InterruptedException {
-        // read device model number
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(TEST_OBJECT_ID, 0, STRING_RESOURCE_INSTANCE_ID, 0), 1000000);
-
-        // verify result
-        assertEquals(CONTENT, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        LwM2mResourceInstance resourceInstance = (LwM2mResourceInstance) response.getContent();
-        assertEquals(0, resourceInstance.getId());
-        assertEquals(MULTI_INSTANCE, resourceInstance.getValue());
-    }
-
-    @Test
-    public void can_read_resource_json_instance() throws InterruptedException {
-        // read device model number
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(ContentFormat.JSON, TEST_OBJECT_ID, 0, STRING_RESOURCE_INSTANCE_ID, 0), 1000000);
-
-        // verify result
-        assertEquals(CONTENT, response.getCode());
-        Response coapResponse = (Response) response.getCoapResponse();
-        assertNotNull(coapResponse);
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        LwM2mResourceInstance resourceInstance = (LwM2mResourceInstance) response.getContent();
-        assertEquals(0, resourceInstance.getId());
-        assertEquals(MULTI_INSTANCE, resourceInstance.getValue());
-    }
-
-    @Test
-    public void cannot_read_non_multiple_resource_json_instance() throws InterruptedException {
-        // read device model number
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(ContentFormat.JSON, TEST_OBJECT_ID, 0, INTEGER_RESOURCE_ID, 0), 1000000);
-
-        // verify result
-        assertEquals(BAD_REQUEST, response.getCode());
-        assertEquals("invalid path : resource is not multiple", response.getErrorMessage());
-    }
-
-    @Test
-    public void can_read_resource_text_instance() throws InterruptedException {
-        // read device model number
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(ContentFormat.TEXT, TEST_OBJECT_ID, 0, STRING_RESOURCE_INSTANCE_ID, 0), 1000000);
-
-        // verify result
-        assertEquals(CONTENT, response.getCode());
-        Response coapResponse = (Response) response.getCoapResponse();
-        assertNotNull(coapResponse);
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        LwM2mResourceInstance resourceInstance = (LwM2mResourceInstance) response.getContent();
-        assertEquals(0, resourceInstance.getId());
-        assertEquals(MULTI_INSTANCE, resourceInstance.getValue());
-    }
-
-    @Test
-    public void can_read_empty_opaque_resource() throws InterruptedException {
-        // read device model number
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(ContentFormat.OPAQUE, TEST_OBJECT_ID, 1, OPAQUE_RESOURCE_ID));
-
-        // verify result
-        assertEquals(CONTENT, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        LwM2mResource resource = (LwM2mResource) response.getContent();
-        assertEquals(Type.OPAQUE, resource.getType());
-        assertEquals(0, ((byte[]) resource.getValue()).length);
-    }
-
-    @Test
-    public void cannot_read_non_readable_resource() throws InterruptedException {
-        // read device reboot resource
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 0, 4));
-
-        // verify result
-        assertEquals(METHOD_NOT_ALLOWED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    @Test
-    public void cannot_read_non_existent_object() throws InterruptedException {
-        // read object "50"
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(50));
-
-        // verify result
-        assertEquals(NOT_FOUND, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    @Test
-    public void cannot_read_non_existent_instance() throws InterruptedException {
-        // read 2nd Device resource
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 1));
-
-        // verify result
-        assertEquals(NOT_FOUND, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    @Test
-    public void cannot_read_non_existent_resource() throws InterruptedException {
-        // read device 50 resource
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 0, 50));
-
-        // verify result
-        assertEquals(NOT_FOUND, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    @Test
-    public void cannot_read_security_resource() throws InterruptedException {
-        // read device 50 resource
-        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(0, 0, 0));
-
-        // verify result
-        assertEquals(NOT_FOUND, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/TestUtil.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/TestUtil.java
new file mode 100644
index 00000000..8ce79034
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/TestUtil.java
@@ -0,0 +1,35 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.*;
+
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.response.LwM2mResponse;
+
+public class TestUtil {
+    public static void assertContentFormat(ContentFormat expectedContentFormat, LwM2mResponse response) {
+        assertNotNull("response must not be null", response);
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+        Response coapResponse = (Response) response.getCoapResponse();
+        assertTrue("response must have content format", coapResponse.getOptions().hasContentFormat());
+        assertEquals(expectedContentFormat.getCode(), coapResponse.getOptions().getContentFormat());
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/WriteTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/WriteTest.java
deleted file mode 100644
index 077900f4..00000000
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/WriteTest.java
+++ /dev/null
@@ -1,680 +0,0 @@
-/*******************************************************************************
- * 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 v2.0
- * and Eclipse Distribution License v1.0 which accompany this distribution.
- * 
- * The Eclipse Public License is available at
- *    http://www.eclipse.org/legal/epl-v20.html
- * and the Eclipse Distribution License is available at
- *    http://www.eclipse.org/org/documents/edl-v10.html.
- * 
- * Contributors:
- *     Zebra Technologies - initial API and implementation
- *     Achim Kraus (Bosch Software Innovations GmbH) - add test for write security object
- *     Achim Kraus (Bosch Software Innovations GmbH) - add test for update and replace instances
- *******************************************************************************/
-package org.eclipse.leshan.integration.tests;
-
-import static org.eclipse.leshan.core.ResponseCode.CHANGED;
-import static org.eclipse.leshan.integration.tests.IntegrationTestHelper.*;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.Is.is;
-import static org.hamcrest.core.IsInstanceOf.instanceOf;
-import static org.junit.Assert.*;
-
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.eclipse.californium.core.coap.Response;
-import org.eclipse.leshan.core.ResponseCode;
-import org.eclipse.leshan.core.model.ResourceModel.Type;
-import org.eclipse.leshan.core.node.LwM2mMultipleResource;
-import org.eclipse.leshan.core.node.LwM2mObjectInstance;
-import org.eclipse.leshan.core.node.LwM2mResource;
-import org.eclipse.leshan.core.node.LwM2mResourceInstance;
-import org.eclipse.leshan.core.node.LwM2mSingleResource;
-import org.eclipse.leshan.core.node.ObjectLink;
-import org.eclipse.leshan.core.node.codec.CodecException;
-import org.eclipse.leshan.core.request.ContentFormat;
-import org.eclipse.leshan.core.request.ReadRequest;
-import org.eclipse.leshan.core.request.WriteRequest;
-import org.eclipse.leshan.core.request.WriteRequest.Mode;
-import org.eclipse.leshan.core.response.ErrorCallback;
-import org.eclipse.leshan.core.response.ReadResponse;
-import org.eclipse.leshan.core.response.ResponseCallback;
-import org.eclipse.leshan.core.response.WriteResponse;
-import org.eclipse.leshan.core.util.datatype.ULong;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
-public class WriteTest {
-    private IntegrationTestHelper helper = new IntegrationTestHelper();
-
-    @Before
-    public void start() {
-        helper.initialize();
-        helper.createServer();
-        helper.server.start();
-        helper.createClient();
-        helper.client.start();
-        helper.waitForRegistrationAtServerSide(1);
-    }
-
-    @After
-    public void stop() {
-        helper.client.destroy(false);
-        helper.server.destroy();
-        helper.dispose();
-    }
-
-    @Test
-    public void can_write_string_resource_in_text() throws InterruptedException {
-        write_string_resource(ContentFormat.TEXT);
-    }
-
-    @Test
-    public void can_write_string_resource_in_tlv() throws InterruptedException {
-        write_string_resource(ContentFormat.TLV);
-    }
-
-    @Test
-    public void can_write_string_resource_in__old_tlv() throws InterruptedException {
-        write_string_resource(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE));
-    }
-
-    @Test
-    public void can_write_string_resource_in_json() throws InterruptedException {
-        write_string_resource(ContentFormat.JSON);
-    }
-
-    @Test
-    public void can_write_string_resource_in__old_json() throws InterruptedException {
-        write_string_resource(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE));
-    }
-
-    private void write_string_resource(ContentFormat format) throws InterruptedException {
-        // write resource
-        String expectedvalue = "stringvalue";
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(format, TEST_OBJECT_ID, 0, STRING_RESOURCE_ID, expectedvalue));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read resource to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(format, TEST_OBJECT_ID, 0, STRING_RESOURCE_ID));
-        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
-        assertEquals(expectedvalue, resource.getValue());
-    }
-
-    @Test
-    public void can_write_boolean_resource_in_text() throws InterruptedException {
-        write_boolean_resource(ContentFormat.TEXT);
-    }
-
-    @Test
-    public void can_write_boolean_resource_in_tlv() throws InterruptedException {
-        write_boolean_resource(ContentFormat.TLV);
-    }
-
-    @Test
-    public void can_write_boolean_resource_in_old_tlv() throws InterruptedException {
-        write_boolean_resource(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE));
-    }
-
-    @Test
-    public void can_write_boolean_resource_in_json() throws InterruptedException {
-        write_boolean_resource(ContentFormat.JSON);
-    }
-
-    @Test
-    public void can_write_boolean_resource_in_old_json() throws InterruptedException {
-        write_boolean_resource(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE));
-    }
-
-    private void write_boolean_resource(ContentFormat format) throws InterruptedException {
-        // write resource
-        boolean expectedvalue = true;
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(format, TEST_OBJECT_ID, 0, BOOLEAN_RESOURCE_ID, expectedvalue));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read resource to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(format, TEST_OBJECT_ID, 0, BOOLEAN_RESOURCE_ID));
-        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
-        assertEquals(expectedvalue, resource.getValue());
-    }
-
-    @Test
-    public void can_write_integer_resource_in_text() throws InterruptedException {
-        write_integer_resource(ContentFormat.TEXT);
-    }
-
-    @Test
-    public void can_write_integer_resource_in_tlv() throws InterruptedException {
-        write_integer_resource(ContentFormat.TLV);
-    }
-
-    @Test
-    public void can_write_integer_resource_in_old_tlv() throws InterruptedException {
-        write_integer_resource(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE));
-    }
-
-    @Test
-    public void can_write_integer_resource_in_json() throws InterruptedException {
-        write_integer_resource(ContentFormat.JSON);
-    }
-
-    @Test
-    public void can_write_integer_resource_in_old_json() throws InterruptedException {
-        write_integer_resource(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE));
-    }
-
-    private void write_integer_resource(ContentFormat format) throws InterruptedException {
-        // write resource
-        long expectedvalue = 999l;
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(format, TEST_OBJECT_ID, 0, INTEGER_RESOURCE_ID, expectedvalue));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read resource to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(format, TEST_OBJECT_ID, 0, INTEGER_RESOURCE_ID));
-        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
-        assertEquals(expectedvalue, resource.getValue());
-    }
-
-    @Test
-    public void can_write_string_resource_json_instance() throws InterruptedException {
-        write_string_resource_instance(ContentFormat.JSON, 0);
-        write_string_resource_instance(ContentFormat.JSON, 1);
-    }
-
-    @Test
-    public void can_write_string_resource_old_json_instance() throws InterruptedException {
-        write_string_resource_instance(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE), 0);
-        write_string_resource_instance(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE), 1);
-    }
-
-    @Test
-    public void can_write_string_resource_text_instance() throws InterruptedException {
-        write_string_resource_instance(ContentFormat.TEXT, 0);
-        write_string_resource_instance(ContentFormat.TEXT, 1);
-    }
-
-    @Test
-    public void can_write_string_multiple_resource_text_instance() throws InterruptedException {
-        write_string_resource_instance(ContentFormat.TEXT, 0);
-        write_string_resource_instance(ContentFormat.TEXT, 1);
-    }
-
-    @Test
-    public void can_write_string_resource_tlv_instance() throws InterruptedException {
-        write_string_resource_instance(ContentFormat.TLV, 0);
-        write_string_resource_instance(ContentFormat.TLV, 1);
-    }
-
-    @Test
-    public void can_write_string_resource_old_tlv_instance() throws InterruptedException {
-        write_string_resource_instance(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE), 0);
-        write_string_resource_instance(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE), 1);
-    }
-
-    private void write_string_resource_instance(ContentFormat format, int resourceInstance)
-            throws InterruptedException {
-        // read device model number
-        String valueToWrite = "newValue";
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(), new WriteRequest(format,
-                TEST_OBJECT_ID, 0, STRING_RESOURCE_INSTANCE_ID, resourceInstance, valueToWrite, Type.STRING));
-
-        // verify result
-        assertEquals(CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read resource to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(format, TEST_OBJECT_ID, 0, STRING_RESOURCE_INSTANCE_ID, resourceInstance));
-
-        // verify result
-        LwM2mResourceInstance resource = (LwM2mResourceInstance) readResponse.getContent();
-        System.out.println(resource);
-        assertEquals(valueToWrite, resource.getValue());
-    }
-
-    @Test
-    public void can_write_float_resource_in_text() throws InterruptedException {
-        write_float_resource(ContentFormat.TEXT);
-    }
-
-    @Test
-    public void can_write_float_resource_in_tlv() throws InterruptedException {
-        write_float_resource(ContentFormat.TLV);
-    }
-
-    @Test
-    public void can_write_float_resource_in_old_tlv() throws InterruptedException {
-        write_float_resource(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE));
-    }
-
-    @Test
-    public void can_write_float_resource_in_json() throws InterruptedException {
-        write_float_resource(ContentFormat.JSON);
-    }
-
-    @Test
-    public void can_write_float_resource_in_old_json() throws InterruptedException {
-        write_float_resource(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE));
-    }
-
-    private void write_float_resource(ContentFormat format) throws InterruptedException {
-        // write resource
-        double expectedvalue = 999.99;
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(format, TEST_OBJECT_ID, 0, FLOAT_RESOURCE_ID, expectedvalue));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read resource to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(format, TEST_OBJECT_ID, 0, FLOAT_RESOURCE_ID));
-        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
-        assertEquals(expectedvalue, resource.getValue());
-    }
-
-    @Test
-    public void can_write_time_resource_in_text() throws InterruptedException {
-        write_time_resource(ContentFormat.TEXT);
-    }
-
-    @Test
-    public void can_write_time_resource_in_tlv() throws InterruptedException {
-        write_time_resource(ContentFormat.TLV);
-    }
-
-    @Test
-    public void can_write_time_resource_in_old_tlv() throws InterruptedException {
-        write_time_resource(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE));
-    }
-
-    @Test
-    public void can_write_time_resource_in_json() throws InterruptedException {
-        write_time_resource(ContentFormat.JSON);
-    }
-
-    @Test
-    public void can_write_time_resource_in_old_json() throws InterruptedException {
-        write_time_resource(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE));
-    }
-
-    private void write_time_resource(ContentFormat format) throws InterruptedException {
-        // write resource
-        Date expectedvalue = new Date(946681000l); // second accuracy
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(format, TEST_OBJECT_ID, 0, TIME_RESOURCE_ID, expectedvalue));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read resource to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(format, TEST_OBJECT_ID, 0, TIME_RESOURCE_ID));
-        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
-        assertEquals(expectedvalue, resource.getValue());
-    }
-
-    @Test
-    public void can_write_opaque_resource_in_opaque() throws InterruptedException {
-        write_opaque_resource(ContentFormat.OPAQUE);
-    }
-
-    @Test
-    public void can_write_opaque_resource_in_tlv() throws InterruptedException {
-        write_opaque_resource(ContentFormat.TLV);
-    }
-
-    @Test
-    public void can_write_opaque_resource_in_old_tlv() throws InterruptedException {
-        write_opaque_resource(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE));
-    }
-
-    @Test
-    public void can_write_opaque_resource_in_json() throws InterruptedException {
-        write_opaque_resource(ContentFormat.JSON);
-    }
-
-    @Test
-    public void can_write_opaque_resource_in_text() throws InterruptedException {
-        write_opaque_resource(ContentFormat.TEXT);
-    }
-
-    @Test
-    public void can_write_opaque_resource_in_old_json() throws InterruptedException {
-        write_opaque_resource(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE));
-    }
-
-    private void write_opaque_resource(ContentFormat format) throws InterruptedException {
-        // write resource
-        byte[] expectedvalue = new byte[] { 1, 2, 3 };
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(format, TEST_OBJECT_ID, 0, OPAQUE_RESOURCE_ID, expectedvalue));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read resource to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(format, TEST_OBJECT_ID, 0, OPAQUE_RESOURCE_ID));
-        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
-        assertArrayEquals(expectedvalue, (byte[]) resource.getValue());
-    }
-
-    @Test
-    public void can_write_unsigned_integer_resource_in_text() throws InterruptedException {
-        write_unsigned_integer_resource(ContentFormat.TEXT);
-    }
-
-    @Test
-    public void can_write_unsigned_integer_resource_in_tlv() throws InterruptedException {
-        write_unsigned_integer_resource(ContentFormat.TLV);
-    }
-
-    @Test
-    public void can_write_unsigned_integer_resource_in_old_tlv() throws InterruptedException {
-        write_unsigned_integer_resource(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE));
-    }
-
-    @Test
-    public void can_write_unsigned_integer_resource_in_json() throws InterruptedException {
-        write_unsigned_integer_resource(ContentFormat.JSON);
-    }
-
-    @Test
-    public void can_write_unsigned_integer_resource_in_old_json() throws InterruptedException {
-        write_unsigned_integer_resource(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE));
-    }
-
-    private void write_unsigned_integer_resource(ContentFormat format) throws InterruptedException {
-        // write resource
-        ULong expectedvalue = ULong.valueOf("18446744073709551615"); // this unsigned integer can not be stored in a
-
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(format, TEST_OBJECT_ID, 0, UNSIGNED_INTEGER_RESOURCE_ID, expectedvalue));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read resource to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(TEST_OBJECT_ID, 0, UNSIGNED_INTEGER_RESOURCE_ID));
-        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
-        assertEquals(expectedvalue, resource.getValue());
-    }
-
-    @Test
-    public void cannot_write_non_writable_resource() throws InterruptedException {
-        // try to write unwritable resource like manufacturer on device
-        String manufacturer = "new manufacturer";
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(3, 0, 0, manufacturer));
-
-        // verify result
-        assertEquals(ResponseCode.METHOD_NOT_ALLOWED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    @Test
-    public void cannot_write_security_resource() throws InterruptedException {
-        // try to write unwritable resource like manufacturer on device
-        String uri = "new.dest.server";
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(), new WriteRequest(0, 0, 0, uri));
-
-        // verify result
-        assertEquals(ResponseCode.NOT_FOUND, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    @Test
-    public void can_write_object_instance_in_tlv() throws InterruptedException {
-        can_write_object_instance(ContentFormat.TLV);
-    }
-
-    @Test
-    public void can_write_object_instance_in_old_tlv() throws InterruptedException {
-        can_write_object_instance(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE));
-    }
-
-    @Test
-    public void can_write_object_instance_in_json() throws InterruptedException {
-        can_write_object_instance(ContentFormat.JSON);
-    }
-
-    @Test
-    public void can_write_object_instance_in_old_json() throws InterruptedException {
-        can_write_object_instance(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE));
-    }
-
-    public void can_write_object_instance(ContentFormat format) throws InterruptedException {
-        // write device timezone and offset
-        LwM2mResource utcOffset = LwM2mSingleResource.newStringResource(14, "+02");
-        LwM2mResource timeZone = LwM2mSingleResource.newStringResource(15, "Europe/Paris");
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(Mode.REPLACE, format, 3, 0, utcOffset, timeZone));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read the timezone to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 0));
-        LwM2mObjectInstance instance = (LwM2mObjectInstance) readResponse.getContent();
-        assertEquals(utcOffset, instance.getResource(14));
-        assertEquals(timeZone, instance.getResource(15));
-    }
-
-    @Test
-    public void can_write_replacing_object_instance() throws InterruptedException {
-        // setup server object
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(), new WriteRequest(1, 0, 3, 60));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // write server object
-        LwM2mResource lifetime = LwM2mSingleResource.newIntegerResource(1, 120);
-        LwM2mResource defaultMinPeriod = LwM2mSingleResource.newIntegerResource(2, 10);
-        LwM2mResource notificationStoring = LwM2mSingleResource.newBooleanResource(6, false);
-        LwM2mResource binding = LwM2mSingleResource.newStringResource(7, "U");
-        response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(Mode.REPLACE, 1, 0, lifetime, defaultMinPeriod, notificationStoring, binding));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read the values to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(1, 0));
-        LwM2mObjectInstance instance = (LwM2mObjectInstance) readResponse.getContent();
-        assertEquals(lifetime, instance.getResource(1));
-        assertEquals(defaultMinPeriod, instance.getResource(2));
-        assertEquals(notificationStoring, instance.getResource(6));
-        assertEquals(binding, instance.getResource(7));
-        assertNull(instance.getResource(3)); // removed not contained optional writable resource
-    }
-
-    @Test
-    public void cannot_write_replacing_incomplete_object_instance() throws InterruptedException {
-        // write server object
-        LwM2mResource lifetime = LwM2mSingleResource.newIntegerResource(1, 120);
-        LwM2mResource defaultMinPeriod = LwM2mSingleResource.newIntegerResource(2, 10);
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(Mode.REPLACE, 1, 0, lifetime, defaultMinPeriod));
-
-        // verify result
-        assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    @Test
-    public void can_write_updating_object_instance() throws InterruptedException {
-        // setup server object
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(), new WriteRequest(1, 0, 3, 60));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-        // write server object
-        LwM2mResource lifetime = LwM2mSingleResource.newIntegerResource(1, 120);
-        LwM2mResource defaultMinPeriod = LwM2mSingleResource.newIntegerResource(2, 10);
-        response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(Mode.UPDATE, 1, 0, lifetime, defaultMinPeriod));
-
-        // verify result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read the values to check the value changed
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(1, 0));
-        LwM2mObjectInstance instance = (LwM2mObjectInstance) readResponse.getContent();
-        assertEquals(lifetime, instance.getResource(1));
-        assertEquals(defaultMinPeriod, instance.getResource(2));
-        // no resources are removed when updating
-        assertNotNull(instance.getResource(3));
-        assertNotNull(instance.getResource(6));
-        assertNotNull(instance.getResource(7));
-    }
-
-    @Test
-    public void can_write_multi_instance_objlnk_resource_in_tlv() throws InterruptedException {
-        Map<Integer, ObjectLink> neighbourCellReport = new HashMap<>();
-        neighbourCellReport.put(0, new ObjectLink(10245, 1));
-        neighbourCellReport.put(1, new ObjectLink(10242, 2));
-        neighbourCellReport.put(2, new ObjectLink(10244, 3));
-
-        // Write objlnk resource in TLV format
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(ContentFormat.TLV, IntegrationTestHelper.TEST_OBJECT_ID, 0,
-                        IntegrationTestHelper.OBJLNK_MULTI_INSTANCE_RESOURCE_ID, neighbourCellReport, Type.OBJLNK));
-
-        // Verify Write result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // Reading back the written OBJLNK value
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(
-                IntegrationTestHelper.TEST_OBJECT_ID, 0, IntegrationTestHelper.OBJLNK_MULTI_INSTANCE_RESOURCE_ID));
-        LwM2mMultipleResource resource = (LwM2mMultipleResource) readResponse.getContent();
-
-        // verify read value
-        assertEquals(((ObjectLink) resource.getValue(0)).getObjectId(), 10245);
-        assertEquals(((ObjectLink) resource.getValue(0)).getObjectInstanceId(), 1);
-        assertEquals(((ObjectLink) resource.getValue(1)).getObjectId(), 10242);
-        assertEquals(((ObjectLink) resource.getValue(1)).getObjectInstanceId(), 2);
-        assertEquals(((ObjectLink) resource.getValue(2)).getObjectId(), 10244);
-        assertEquals(((ObjectLink) resource.getValue(2)).getObjectInstanceId(), 3);
-    }
-
-    @Test
-    public void can_write_single_instance_objlnk_resource_in_tlv() throws InterruptedException {
-        ObjectLink data = new ObjectLink(10245, 1);
-
-        // Write objlnk resource in TLV format
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(ContentFormat.TLV, IntegrationTestHelper.TEST_OBJECT_ID, 0,
-                        IntegrationTestHelper.OBJLNK_SINGLE_INSTANCE_RESOURCE_ID, data));
-
-        // Verify Write result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // Reading back the written OBJLNK value
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(
-                IntegrationTestHelper.TEST_OBJECT_ID, 0, IntegrationTestHelper.OBJLNK_SINGLE_INSTANCE_RESOURCE_ID));
-        LwM2mSingleResource resource = (LwM2mSingleResource) readResponse.getContent();
-
-        // verify read value
-        assertEquals(((ObjectLink) resource.getValue()).getObjectId(), 10245);
-        assertEquals(((ObjectLink) resource.getValue()).getObjectInstanceId(), 1);
-    }
-
-    @Test
-    public void can_write_single_instance_objlnk_resource_in_text() throws InterruptedException {
-        // Write objlnk resource in TEXT format
-        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(ContentFormat.TEXT, IntegrationTestHelper.TEST_OBJECT_ID, 0,
-                        IntegrationTestHelper.OBJLNK_SINGLE_INSTANCE_RESOURCE_ID, new ObjectLink(10245, 0)));
-
-        // Verify Write result
-        assertEquals(ResponseCode.CHANGED, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // Reading back the written OBJLNK value
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(ContentFormat.TEXT, IntegrationTestHelper.TEST_OBJECT_ID, 0,
-                        IntegrationTestHelper.OBJLNK_SINGLE_INSTANCE_RESOURCE_ID));
-        LwM2mSingleResource resource = (LwM2mSingleResource) readResponse.getContent();
-
-        // verify read value
-        assertEquals(((ObjectLink) resource.getValue()).getObjectId(), 10245);
-        assertEquals(((ObjectLink) resource.getValue()).getObjectInstanceId(), 0);
-    }
-
-    @Test(expected = CodecException.class)
-    public void send_writerequest_synchronously_with_bad_payload_raises_codeexception() throws InterruptedException {
-        helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(3, 0, 13, "a string instead of timestamp for currenttime resource"));
-
-    }
-
-    @Test(expected = CodecException.class)
-    public void send_writerequest_asynchronously_with_bad_payload_raises_codeexception() throws InterruptedException {
-        helper.server.send(helper.getCurrentRegistration(),
-                new WriteRequest(3, 0, 13, "a string instead of timestamp for currenttime resource"),
-                new ResponseCallback<WriteResponse>() {
-                    @Override
-                    public void onResponse(WriteResponse response) {
-                    }
-                }, new ErrorCallback() {
-                    @Override
-                    public void onError(Exception e) {
-                    }
-                });
-    }
-}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateFailedTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateFailedTest.java
new file mode 100644
index 00000000..694ea693
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateFailedTest.java
@@ -0,0 +1,88 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.create;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.*;
+
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.leshan.core.ResponseCode;
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.request.CreateRequest;
+import org.eclipse.leshan.core.response.CreateResponse;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class CreateFailedTest {
+    public IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void cannot_create_mandatory_single_object() throws InterruptedException {
+        // try to create another instance of device object
+        CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new CreateRequest(3, new LwM2mResource[] { LwM2mSingleResource.newStringResource(3, "v123") }));
+
+        // verify result
+        assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+
+    @Test
+    public void cannot_create_instance_of_security_object() throws InterruptedException {
+        CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new CreateRequest(0, new LwM2mResource[] { LwM2mSingleResource.newStringResource(0, "new.dest") }));
+
+        // verify result
+        assertEquals(ResponseCode.NOT_FOUND, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+
+    @Test
+    public void cannot_create_instance_of_absent_object() throws InterruptedException {
+        // try to create an instance of object 50
+        CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new CreateRequest(50, new LwM2mResource[0]));
+
+        // verify result
+        assertEquals(ResponseCode.NOT_FOUND, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/CreateTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java
similarity index 56%
rename from leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/CreateTest.java
rename to leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java
index bb376983..f1dfa8d7 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/CreateTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/create/CreateTest.java
@@ -16,15 +16,17 @@
  *     Achim Kraus (Bosch Software Innovations GmbH) - replace close() with destroy()
  *******************************************************************************/
 
-package org.eclipse.leshan.integration.tests;
+package org.eclipse.leshan.integration.tests.create;
 
-import static org.eclipse.leshan.core.request.ContentFormat.*;
 import static org.eclipse.leshan.integration.tests.IntegrationTestHelper.*;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.Is.is;
 import static org.hamcrest.core.IsInstanceOf.instanceOf;
 import static org.junit.Assert.*;
 
+import java.util.Arrays;
+import java.util.Collection;
+
 import org.eclipse.californium.core.coap.Response;
 import org.eclipse.leshan.core.ResponseCode;
 import org.eclipse.leshan.core.node.LwM2mObject;
@@ -37,13 +39,31 @@ import org.eclipse.leshan.core.request.ReadRequest;
 import org.eclipse.leshan.core.request.exception.InvalidRequestException;
 import org.eclipse.leshan.core.response.CreateResponse;
 import org.eclipse.leshan.core.response.ReadResponse;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 
+@RunWith(Parameterized.class)
 public class CreateTest {
+    protected IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    @Parameters(name = "{0}")
+    public static Collection<?> contentFormats() {
+        return Arrays.asList(new Object[][] { //
+                                { ContentFormat.TLV }, //
+                                { ContentFormat.JSON }, //
+                                { ContentFormat.SENML_JSON } });
+    }
 
-    IntegrationTestHelper helper = new IntegrationTestHelper();
+    private ContentFormat contentFormat;
+
+    public CreateTest(ContentFormat contentFormat) {
+        this.contentFormat = contentFormat;
+    }
 
     @Before
     public void start() {
@@ -63,59 +83,46 @@ public class CreateTest {
     }
 
     @Test
-    public void can_create_instance_without_instance_id_tlv() throws InterruptedException {
-        can_create_instance_without_instance_id(TLV);
-    }
-
-    @Test(expected = InvalidRequestException.class)
-    public void can_create_instance_without_instance_id_json() throws InterruptedException {
-        can_create_instance_without_instance_id(JSON);
-    }
-
-    public void can_create_instance_without_instance_id(ContentFormat format) throws InterruptedException {
-        // create ACL instance
-        CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new CreateRequest(format, 2, LwM2mSingleResource.newIntegerResource(3, 33)));
-
-        // verify result
-        assertEquals(ResponseCode.CREATED, response.getCode());
-        assertEquals("2/0", response.getLocation());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // create a second ACL instance
-        response = helper.server.send(helper.getCurrentRegistration(),
-                new CreateRequest(format, 2, LwM2mSingleResource.newIntegerResource(3, 34)));
-
-        // verify result
-        assertEquals(ResponseCode.CREATED, response.getCode());
-        assertEquals("2/1", response.getLocation());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // read object 2
-        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(2));
-        assertEquals(ResponseCode.CONTENT, readResponse.getCode());
-        LwM2mObject object = (LwM2mObject) readResponse.getContent();
-        assertEquals(33l, object.getInstance(0).getResource(3).getValue());
-        assertEquals(34l, object.getInstance(1).getResource(3).getValue());
+    public void can_create_instance_without_instance_id() throws InterruptedException {
+        try {
+            // create ACL instance
+            CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
+                    new CreateRequest(contentFormat, 2, LwM2mSingleResource.newIntegerResource(3, 33)));
+
+            // verify result
+            assertEquals(ResponseCode.CREATED, response.getCode());
+            assertEquals("2/0", response.getLocation());
+            assertNotNull(response.getCoapResponse());
+            assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+            // create a second ACL instance
+            response = helper.server.send(helper.getCurrentRegistration(),
+                    new CreateRequest(contentFormat, 2, LwM2mSingleResource.newIntegerResource(3, 34)));
+
+            // verify result
+            assertEquals(ResponseCode.CREATED, response.getCode());
+            assertEquals("2/1", response.getLocation());
+            assertNotNull(response.getCoapResponse());
+            assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+            // read object 2
+            ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(2));
+            assertEquals(ResponseCode.CONTENT, readResponse.getCode());
+            LwM2mObject object = (LwM2mObject) readResponse.getContent();
+            assertEquals(33l, object.getInstance(0).getResource(3).getValue());
+            assertEquals(34l, object.getInstance(1).getResource(3).getValue());
+        } catch (InvalidRequestException e) {
+            // only TLV support create instance without instance id
+            assertNotEquals(ContentFormat.TLV, contentFormat);
+        }
     }
 
     @Test
-    public void can_create_instance_with_id_tlv() throws InterruptedException {
-        can_create_instance_with_id(TLV);
-    }
-
-    @Test
-    public void can_create_instance_with_id_json() throws InterruptedException {
-        can_create_instance_with_id(JSON);
-    }
-
-    public void can_create_instance_with_id(ContentFormat format) throws InterruptedException {
+    public void can_create_instance_with_id() throws InterruptedException {
         // create ACL instance
         LwM2mObjectInstance instance = new LwM2mObjectInstance(12, LwM2mSingleResource.newIntegerResource(3, 123));
         CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new CreateRequest(format, 2, instance));
+                new CreateRequest(contentFormat, 2, instance));
 
         // verify result
         assertEquals(ResponseCode.CREATED, response.getCode());
@@ -131,21 +138,12 @@ public class CreateTest {
     }
 
     @Test
-    public void can_create_2_instances_of_object_tlv() throws InterruptedException {
-        can_create_2_instances_of_object(TLV);
-    }
-
-    @Test
-    public void can_create_2_instances_of_object_json() throws InterruptedException {
-        can_create_2_instances_of_object(JSON);
-    }
-
-    public void can_create_2_instances_of_object(ContentFormat format) throws InterruptedException {
+    public void can_create_2_instances_of_object() throws InterruptedException {
         // create ACL instance
         LwM2mObjectInstance instance1 = new LwM2mObjectInstance(12, LwM2mSingleResource.newIntegerResource(3, 123));
         LwM2mObjectInstance instance2 = new LwM2mObjectInstance(13, LwM2mSingleResource.newIntegerResource(3, 124));
         CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new CreateRequest(format, 2, instance1, instance2));
+                new CreateRequest(contentFormat, 2, instance1, instance2));
 
         // verify result
         assertEquals(ResponseCode.CREATED, response.getCode());
@@ -161,23 +159,11 @@ public class CreateTest {
         assertEquals(object.getInstance(13).getResource(3).getValue(), 124l);
     }
 
-    @Test
-    public void cannot_create_instance_of_absent_object() throws InterruptedException {
-        // try to create an instance of object 50
-        CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new CreateRequest(50, new LwM2mResource[0]));
-
-        // verify result
-        assertEquals(ResponseCode.NOT_FOUND, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
     @Test
     public void cannot_create_instance_without_all_required_resources() throws InterruptedException {
         // create ACL instance without any resources
         CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new CreateRequest(TEST_OBJECT_ID, new LwM2mResource[0]));
+                new CreateRequest(contentFormat, TEST_OBJECT_ID, new LwM2mObjectInstance(0, new LwM2mResource[0])));
 
         // verify result
         assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
@@ -185,8 +171,9 @@ public class CreateTest {
         assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
 
         // create ACL instance with only 1 mandatory resources (1 missing)
-        CreateResponse response2 = helper.server.send(helper.getCurrentRegistration(), new CreateRequest(TEST_OBJECT_ID,
-                LwM2mSingleResource.newIntegerResource(INTEGER_MANDATORY_RESOURCE_ID, 12)));
+        CreateResponse response2 = helper.server.send(helper.getCurrentRegistration(), new CreateRequest(contentFormat,
+                TEST_OBJECT_ID,
+                new LwM2mObjectInstance(0, LwM2mSingleResource.newIntegerResource(INTEGER_MANDATORY_RESOURCE_ID, 12))));
 
         // verify result
         assertEquals(ResponseCode.BAD_REQUEST, response2.getCode());
@@ -199,7 +186,7 @@ public class CreateTest {
                 LwM2mSingleResource.newIntegerResource(INTEGER_MANDATORY_RESOURCE_ID, 22));
 
         CreateResponse response3 = helper.server.send(helper.getCurrentRegistration(),
-                new CreateRequest(TEST_OBJECT_ID, instance0, instance1));
+                new CreateRequest(contentFormat, TEST_OBJECT_ID, instance0, instance1));
 
         // verify result
         assertEquals(ResponseCode.BAD_REQUEST, response3.getCode());
@@ -211,28 +198,4 @@ public class CreateTest {
         assertNotNull(response.getCoapResponse());
         assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
     }
-
-    @Test
-    public void cannot_create_mandatory_single_object() throws InterruptedException {
-        // try to create another instance of device object
-        CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new CreateRequest(3, new LwM2mResource[] { LwM2mSingleResource.newStringResource(3, "v123") }));
-
-        // verify result
-        assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    @Test
-    public void cannot_create_instance_of_security_object() throws InterruptedException {
-        CreateResponse response = helper.server.send(helper.getCurrentRegistration(),
-                new CreateRequest(0, new LwM2mResource[] { LwM2mSingleResource.newStringResource(0, "new.dest") }));
-
-        // verify result
-        assertEquals(ResponseCode.NOT_FOUND, response.getCode());
-        assertNotNull(response.getCoapResponse());
-        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
 }
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/observe/ObserveTest.java
similarity index 56%
rename from leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/ObserveTest.java
rename to leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTest.java
index 7bdb9d95..b5d627e4 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/observe/ObserveTest.java
@@ -17,38 +17,22 @@
  *                                                     instead of address for response
  *******************************************************************************/
 
-package org.eclipse.leshan.integration.tests;
+package org.eclipse.leshan.integration.tests.observe;
 
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.Is.is;
 import static org.hamcrest.core.IsInstanceOf.instanceOf;
 import static org.junit.Assert.*;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Set;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
 
-import org.eclipse.californium.core.coap.CoAP.Type;
-import org.eclipse.californium.core.coap.OptionSet;
 import org.eclipse.californium.core.coap.Response;
-import org.eclipse.californium.core.network.CoapEndpoint;
-import org.eclipse.californium.core.network.serialization.UdpDataSerializer;
-import org.eclipse.californium.elements.AddressEndpointContext;
-import org.eclipse.californium.elements.Connector;
-import org.eclipse.californium.elements.EndpointContext;
-import org.eclipse.californium.elements.RawData;
-import org.eclipse.leshan.client.californium.LeshanClient;
-import org.eclipse.leshan.client.servers.ServerIdentity;
 import org.eclipse.leshan.core.ResponseCode;
 import org.eclipse.leshan.core.model.StaticModel;
 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.LwM2mSingleResource;
-import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.node.codec.LwM2mValueChecker;
 import org.eclipse.leshan.core.node.codec.json.LwM2mNodeJsonEncoder;
 import org.eclipse.leshan.core.observation.Observation;
@@ -61,8 +45,7 @@ import org.eclipse.leshan.core.response.CancelObservationResponse;
 import org.eclipse.leshan.core.response.LwM2mResponse;
 import org.eclipse.leshan.core.response.ObserveResponse;
 import org.eclipse.leshan.core.response.ReadResponse;
-import org.eclipse.leshan.server.observation.ObservationListener;
-import org.eclipse.leshan.server.registration.Registration;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -168,7 +151,7 @@ public class ObserveTest {
         // verify result
         listener.waitForNotification(1000);
         assertEquals(ResponseCode.CHANGED, writeResponse.getCode());
-        assertFalse("Observation should be cancelled", listener.receivedNotify.get());
+        assertFalse("Observation should be cancelled", listener.receivedNotify().get());
     }
 
     @Test
@@ -224,7 +207,7 @@ public class ObserveTest {
         // verify result
         listener.waitForNotification(1000);
         assertEquals(ResponseCode.CHANGED, writeResponse.getCode());
-        assertFalse("Observation should be cancelled", listener.receivedNotify.get());
+        assertFalse("Observation should be cancelled", listener.receivedNotify().get());
     }
 
     @Test
@@ -328,9 +311,9 @@ public class ObserveTest {
         byte[] payload = new LwM2mNodeJsonEncoder().encode(LwM2mSingleResource.newStringResource(15, "Paris"),
                 new LwM2mPath("/3/0/15"), new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
         Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
-        sendNotification(getConnector(helper.client, helper.getCurrentRegisteredServer()), payload, firstCoapResponse,
-                666); // 666 is not a supported //
-        // contentFormat.
+        // 666 is not a supported content format.
+        TestObserveUtil.sendNotification(helper.getClientConnector(helper.getCurrentRegisteredServer()),
+                helper.server.getUnsecuredAddress(), payload, firstCoapResponse, ContentFormat.fromCode(666));
         // *** Hack End *** //
 
         // verify result
@@ -339,221 +322,4 @@ public class ObserveTest {
         assertNotNull(listener.getError());
     }
 
-    @Test
-    public void can_observe_timestamped_resource() throws InterruptedException {
-        TestObservationListener listener = new TestObservationListener();
-        helper.server.getObservationService().addListener(listener);
-
-        // observe device timezone
-        ObserveResponse observeResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ObserveRequest(3, 0, 15));
-        assertEquals(ResponseCode.CONTENT, observeResponse.getCode());
-        assertNotNull(observeResponse.getCoapResponse());
-        assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // an observation response should have been sent
-        Observation observation = observeResponse.getObservation();
-        assertEquals("/3/0/15", observation.getPath().toString());
-        assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId());
-        Set<Observation> observations = helper.server.getObservationService()
-                .getObservations(helper.getCurrentRegistration());
-        assertTrue("We should have only on observation", observations.size() == 1);
-        assertTrue("New observation is not there", observations.contains(observation));
-
-        // *** HACK send time-stamped notification as Leshan client does not support it *** //
-        // create time-stamped nodes
-        TimestampedLwM2mNode mostRecentNode = new TimestampedLwM2mNode(System.currentTimeMillis(),
-                LwM2mSingleResource.newStringResource(15, "Paris"));
-        List<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>();
-        timestampedNodes.add(mostRecentNode);
-        timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
-                LwM2mSingleResource.newStringResource(15, "Londres")));
-        byte[] payload = new LwM2mNodeJsonEncoder().encodeTimestampedData(timestampedNodes, new LwM2mPath("/3/0/15"),
-                new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
-        Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
-        sendNotification(getConnector(helper.client, helper.getCurrentRegisteredServer()), payload, firstCoapResponse,
-                ContentFormat.JSON_CODE);
-        // *** Hack End *** //
-
-        // verify result
-        listener.waitForNotification(2000);
-        assertTrue(listener.receivedNotify().get());
-        assertEquals(mostRecentNode.getNode(), listener.getResponse().getContent());
-        assertEquals(timestampedNodes, listener.getResponse().getTimestampedLwM2mNode());
-        assertNotNull(listener.getResponse().getCoapResponse());
-        assertThat(listener.getResponse().getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    @Test
-    public void can_observe_timestamped_instance() throws InterruptedException {
-        TestObservationListener listener = new TestObservationListener();
-        helper.server.getObservationService().addListener(listener);
-
-        // observe device timezone
-        ObserveResponse observeResponse = helper.server.send(helper.getCurrentRegistration(), new ObserveRequest(3, 0));
-        assertEquals(ResponseCode.CONTENT, observeResponse.getCode());
-        assertNotNull(observeResponse.getCoapResponse());
-        assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // an observation response should have been sent
-        Observation observation = observeResponse.getObservation();
-        assertEquals("/3/0", observation.getPath().toString());
-        assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId());
-        Set<Observation> observations = helper.server.getObservationService()
-                .getObservations(helper.getCurrentRegistration());
-        assertTrue("We should have only on observation", observations.size() == 1);
-        assertTrue("New observation is not there", observations.contains(observation));
-
-        // *** HACK send time-stamped notification as Leshan client does not support it *** //
-        // create time-stamped nodes
-        TimestampedLwM2mNode mostRecentNode = new TimestampedLwM2mNode(System.currentTimeMillis(),
-                new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Paris")));
-        List<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>();
-        timestampedNodes.add(mostRecentNode);
-        timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
-                new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres"))));
-        byte[] payload = new LwM2mNodeJsonEncoder().encodeTimestampedData(timestampedNodes, new LwM2mPath("/3/0"),
-                new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
-        Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
-        sendNotification(getConnector(helper.client, helper.getCurrentRegisteredServer()), payload, firstCoapResponse,
-                ContentFormat.JSON_CODE);
-        // *** Hack End *** //
-
-        // verify result
-        listener.waitForNotification(2000);
-        assertTrue(listener.receivedNotify().get());
-        assertEquals(mostRecentNode.getNode(), listener.getResponse().getContent());
-        assertEquals(timestampedNodes, listener.getResponse().getTimestampedLwM2mNode());
-        assertNotNull(listener.getResponse().getCoapResponse());
-        assertThat(listener.getResponse().getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    @Test
-    public void can_observe_timestamped_object() throws InterruptedException {
-        TestObservationListener listener = new TestObservationListener();
-        helper.server.getObservationService().addListener(listener);
-
-        // observe device timezone
-        ObserveResponse observeResponse = helper.server.send(helper.getCurrentRegistration(), new ObserveRequest(3));
-        assertEquals(ResponseCode.CONTENT, observeResponse.getCode());
-        assertNotNull(observeResponse.getCoapResponse());
-        assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class)));
-
-        // an observation response should have been sent
-        Observation observation = observeResponse.getObservation();
-        assertEquals("/3", observation.getPath().toString());
-        assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId());
-        Set<Observation> observations = helper.server.getObservationService()
-                .getObservations(helper.getCurrentRegistration());
-        assertTrue("We should have only on observation", observations.size() == 1);
-        assertTrue("New observation is not there", observations.contains(observation));
-
-        // *** HACK send time-stamped notification as Leshan client does not support it *** //
-        // create time-stamped nodes
-        TimestampedLwM2mNode mostRecentNode = new TimestampedLwM2mNode(System.currentTimeMillis(),
-                new LwM2mObject(3, new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Paris"))));
-        List<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>();
-        timestampedNodes.add(mostRecentNode);
-        timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
-                new LwM2mObject(3, new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres")))));
-        byte[] payload = new LwM2mNodeJsonEncoder().encodeTimestampedData(timestampedNodes, new LwM2mPath("/3"),
-                new StaticModel(helper.createObjectModels()), new LwM2mValueChecker());
-
-        Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
-        sendNotification(getConnector(helper.client, helper.getCurrentRegisteredServer()), payload, firstCoapResponse,
-                ContentFormat.JSON_CODE);
-        // *** Hack End *** //
-
-        // verify result
-        listener.waitForNotification(2000);
-        assertTrue(listener.receivedNotify().get());
-        assertEquals(mostRecentNode.getNode(), listener.getResponse().getContent());
-        assertEquals(timestampedNodes, listener.getResponse().getTimestampedLwM2mNode());
-        assertNotNull(listener.getResponse().getCoapResponse());
-        assertThat(listener.getResponse().getCoapResponse(), is(instanceOf(Response.class)));
-    }
-
-    private void sendNotification(Connector connector, byte[] payload, Response firstCoapResponse, int contentFormat) {
-
-        // create observe response
-        Response response = new Response(org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT);
-        response.setType(Type.NON);
-        response.setPayload(payload);
-        response.setMID(firstCoapResponse.getMID() + 1);
-        response.setToken(firstCoapResponse.getToken());
-        OptionSet options = new OptionSet().setContentFormat(contentFormat)
-                .setObserve(firstCoapResponse.getOptions().getObserve() + 1);
-        response.setOptions(options);
-        EndpointContext context = new AddressEndpointContext(helper.server.getUnsecuredAddress().getAddress(),
-                helper.server.getUnsecuredAddress().getPort());
-        response.setDestinationContext(context);
-
-        // serialize response
-        UdpDataSerializer serializer = new UdpDataSerializer();
-        RawData data = serializer.serializeResponse(response);
-
-        // send it
-        connector.send(data);
-    }
-
-    private Connector getConnector(LeshanClient client, ServerIdentity server) {
-        CoapEndpoint endpoint = (CoapEndpoint) client.coap().getServer().getEndpoint(client.getAddress(server));
-        return endpoint.getConnector();
-    }
-
-    private final class TestObservationListener implements ObservationListener {
-
-        private CountDownLatch latch = new CountDownLatch(1);
-        private final AtomicBoolean receivedNotify = new AtomicBoolean();
-        private ObserveResponse response;
-        private Exception error;
-
-        @Override
-        public void onResponse(Observation observation, Registration registration, ObserveResponse response) {
-            receivedNotify.set(true);
-            this.response = response;
-            this.error = null;
-            latch.countDown();
-        }
-
-        @Override
-        public void onError(Observation observation, Registration registration, Exception error) {
-            receivedNotify.set(true);
-            this.response = null;
-            this.error = error;
-            latch.countDown();
-        }
-
-        @Override
-        public void cancelled(Observation observation) {
-            latch.countDown();
-        }
-
-        @Override
-        public void newObservation(Observation observation, Registration registration) {
-        }
-
-        public AtomicBoolean receivedNotify() {
-            return receivedNotify;
-        }
-
-        public ObserveResponse getResponse() {
-            return response;
-        }
-
-        public Exception getError() {
-            return error;
-        }
-
-        public void waitForNotification(long timeout) throws InterruptedException {
-            latch.await(timeout, TimeUnit.MILLISECONDS);
-        }
-
-        public void reset() {
-            latch = new CountDownLatch(1);
-            receivedNotify.set(false);
-            response = null;
-            error = null;
-        }
-    }
 }
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java
new file mode 100644
index 00000000..22b536ef
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/ObserveTimeStampTest.java
@@ -0,0 +1,218 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.observe;
+
+import static org.eclipse.leshan.integration.tests.TestUtil.assertContentFormat;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.leshan.core.ResponseCode;
+import org.eclipse.leshan.core.model.StaticModel;
+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.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
+import org.eclipse.leshan.core.node.codec.DefaultLwM2mNodeEncoder;
+import org.eclipse.leshan.core.node.codec.LwM2mNodeEncoder;
+import org.eclipse.leshan.core.observation.Observation;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.ObserveRequest;
+import org.eclipse.leshan.core.response.ObserveResponse;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ObserveTimeStampTest {
+
+    @Parameters(name = "{0}")
+    public static Collection<?> contentFormats() {
+        return Arrays.asList(new Object[][] { //
+                                { ContentFormat.JSON }, //
+                                { ContentFormat.SENML_JSON } });
+    }
+
+    protected IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    private ContentFormat contentFormat;
+    private LwM2mNodeEncoder encoder = new DefaultLwM2mNodeEncoder();
+
+    public ObserveTimeStampTest(ContentFormat contentFormat) {
+        this.contentFormat = contentFormat;
+    }
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void can_observe_timestamped_resource() throws InterruptedException {
+        TestObservationListener listener = new TestObservationListener();
+        helper.server.getObservationService().addListener(listener);
+
+        // observe device timezone
+        ObserveResponse observeResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ObserveRequest(3, 0, 15));
+        assertEquals(ResponseCode.CONTENT, observeResponse.getCode());
+        assertNotNull(observeResponse.getCoapResponse());
+        assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // an observation response should have been sent
+        Observation observation = observeResponse.getObservation();
+        assertEquals("/3/0/15", observation.getPath().toString());
+        assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId());
+        Set<Observation> observations = helper.server.getObservationService()
+                .getObservations(helper.getCurrentRegistration());
+        assertTrue("We should have only on observation", observations.size() == 1);
+        assertTrue("New observation is not there", observations.contains(observation));
+
+        // *** HACK send time-stamped notification as Leshan client does not support it *** //
+        // create time-stamped nodes
+        TimestampedLwM2mNode mostRecentNode = new TimestampedLwM2mNode(System.currentTimeMillis(),
+                LwM2mSingleResource.newStringResource(15, "Paris"));
+        List<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>();
+        timestampedNodes.add(mostRecentNode);
+        timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
+                LwM2mSingleResource.newStringResource(15, "Londres")));
+        byte[] payload = encoder.encodeTimestampedData(timestampedNodes, contentFormat, new LwM2mPath("/3/0/15"),
+                new StaticModel(helper.createObjectModels()));
+        Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
+        TestObserveUtil.sendNotification(helper.getClientConnector(helper.getCurrentRegisteredServer()),
+                helper.server.getUnsecuredAddress(), payload, firstCoapResponse, contentFormat);
+        // *** Hack End *** //
+
+        // verify result
+        listener.waitForNotification(2000);
+        assertTrue(listener.receivedNotify().get());
+        assertEquals(mostRecentNode.getNode(), listener.getResponse().getContent());
+        assertEquals(timestampedNodes, listener.getResponse().getTimestampedLwM2mNode());
+        assertContentFormat(contentFormat, listener.getResponse());
+    }
+
+    @Test
+    public void can_observe_timestamped_instance() throws InterruptedException {
+        TestObservationListener listener = new TestObservationListener();
+        helper.server.getObservationService().addListener(listener);
+
+        // observe device timezone
+        ObserveResponse observeResponse = helper.server.send(helper.getCurrentRegistration(), new ObserveRequest(3, 0));
+        assertEquals(ResponseCode.CONTENT, observeResponse.getCode());
+        assertNotNull(observeResponse.getCoapResponse());
+        assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // an observation response should have been sent
+        Observation observation = observeResponse.getObservation();
+        assertEquals("/3/0", observation.getPath().toString());
+        assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId());
+        Set<Observation> observations = helper.server.getObservationService()
+                .getObservations(helper.getCurrentRegistration());
+        assertTrue("We should have only on observation", observations.size() == 1);
+        assertTrue("New observation is not there", observations.contains(observation));
+
+        // *** HACK send time-stamped notification as Leshan client does not support it *** //
+        // create time-stamped nodes
+        TimestampedLwM2mNode mostRecentNode = new TimestampedLwM2mNode(System.currentTimeMillis(),
+                new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Paris")));
+        List<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>();
+        timestampedNodes.add(mostRecentNode);
+        timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
+                new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres"))));
+        byte[] payload = encoder.encodeTimestampedData(timestampedNodes, contentFormat, new LwM2mPath("/3/0"),
+                new StaticModel(helper.createObjectModels()));
+        Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
+        TestObserveUtil.sendNotification(helper.getClientConnector(helper.getCurrentRegisteredServer()),
+                helper.server.getUnsecuredAddress(), payload, firstCoapResponse, contentFormat);
+        // *** Hack End *** //
+
+        // verify result
+        listener.waitForNotification(2000);
+        assertTrue(listener.receivedNotify().get());
+        assertEquals(mostRecentNode.getNode(), listener.getResponse().getContent());
+        assertEquals(timestampedNodes, listener.getResponse().getTimestampedLwM2mNode());
+        assertContentFormat(contentFormat, listener.getResponse());
+    }
+
+    @Test
+    public void can_observe_timestamped_object() throws InterruptedException {
+        TestObservationListener listener = new TestObservationListener();
+        helper.server.getObservationService().addListener(listener);
+
+        // observe device timezone
+        ObserveResponse observeResponse = helper.server.send(helper.getCurrentRegistration(), new ObserveRequest(3));
+        assertEquals(ResponseCode.CONTENT, observeResponse.getCode());
+        assertNotNull(observeResponse.getCoapResponse());
+        assertThat(observeResponse.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // an observation response should have been sent
+        Observation observation = observeResponse.getObservation();
+        assertEquals("/3", observation.getPath().toString());
+        assertEquals(helper.getCurrentRegistration().getId(), observation.getRegistrationId());
+        Set<Observation> observations = helper.server.getObservationService()
+                .getObservations(helper.getCurrentRegistration());
+        assertTrue("We should have only on observation", observations.size() == 1);
+        assertTrue("New observation is not there", observations.contains(observation));
+
+        // *** HACK send time-stamped notification as Leshan client does not support it *** //
+        // create time-stamped nodes
+        TimestampedLwM2mNode mostRecentNode = new TimestampedLwM2mNode(System.currentTimeMillis(),
+                new LwM2mObject(3, new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Paris"))));
+        List<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>();
+        timestampedNodes.add(mostRecentNode);
+        timestampedNodes.add(new TimestampedLwM2mNode(mostRecentNode.getTimestamp() - 2,
+                new LwM2mObject(3, new LwM2mObjectInstance(0, LwM2mSingleResource.newStringResource(15, "Londres")))));
+        byte[] payload = encoder.encodeTimestampedData(timestampedNodes, contentFormat, new LwM2mPath("/3"),
+                new StaticModel(helper.createObjectModels()));
+
+        Response firstCoapResponse = (Response) observeResponse.getCoapResponse();
+        TestObserveUtil.sendNotification(helper.getClientConnector(helper.getCurrentRegisteredServer()),
+                helper.server.getUnsecuredAddress(), payload, firstCoapResponse, contentFormat);
+        // *** Hack End *** //
+
+        // verify result
+        listener.waitForNotification(2000);
+        assertTrue(listener.receivedNotify().get());
+        assertEquals(mostRecentNode.getNode(), listener.getResponse().getContent());
+        assertEquals(timestampedNodes, listener.getResponse().getTimestampedLwM2mNode());
+        assertContentFormat(contentFormat, listener.getResponse());
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RedisObserveTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/RedisObserveTest.java
similarity index 87%
rename from leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RedisObserveTest.java
rename to leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/RedisObserveTest.java
index 22bda2c1..8910f904 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/RedisObserveTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/RedisObserveTest.java
@@ -13,7 +13,9 @@
  * Contributors:
  *     Sierra Wireless - initial API and implementation
  *******************************************************************************/
-package org.eclipse.leshan.integration.tests;
+package org.eclipse.leshan.integration.tests.observe;
+
+import org.eclipse.leshan.integration.tests.RedisIntegrationTestHelper;
 
 public class RedisObserveTest extends ObserveTest {
 
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/TestObservationListener.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/TestObservationListener.java
new file mode 100644
index 00000000..8c1bcf5b
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/TestObservationListener.java
@@ -0,0 +1,66 @@
+package org.eclipse.leshan.integration.tests.observe;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.leshan.core.observation.Observation;
+import org.eclipse.leshan.core.response.ObserveResponse;
+import org.eclipse.leshan.server.observation.ObservationListener;
+import org.eclipse.leshan.server.registration.Registration;
+
+public class TestObservationListener implements ObservationListener {
+
+    private CountDownLatch latch = new CountDownLatch(1);
+    private final AtomicBoolean receivedNotify = new AtomicBoolean();
+    private ObserveResponse response;
+    private Exception error;
+
+    @Override
+    public void onResponse(Observation observation, Registration registration, ObserveResponse response) {
+        receivedNotify.set(true);
+        this.response = response;
+        this.error = null;
+        latch.countDown();
+    }
+
+    @Override
+    public void onError(Observation observation, Registration registration, Exception error) {
+        receivedNotify.set(true);
+        this.response = null;
+        this.error = error;
+        latch.countDown();
+    }
+
+    @Override
+    public void cancelled(Observation observation) {
+        latch.countDown();
+    }
+
+    @Override
+    public void newObservation(Observation observation, Registration registration) {
+    }
+
+    public AtomicBoolean receivedNotify() {
+        return receivedNotify;
+    }
+
+    public ObserveResponse getResponse() {
+        return response;
+    }
+
+    public Exception getError() {
+        return error;
+    }
+
+    public void waitForNotification(long timeout) throws InterruptedException {
+        latch.await(timeout, TimeUnit.MILLISECONDS);
+    }
+
+    public void reset() {
+        latch = new CountDownLatch(1);
+        receivedNotify.set(false);
+        response = null;
+        error = null;
+    }
+}
\ No newline at end of file
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/TestObserveUtil.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/TestObserveUtil.java
new file mode 100644
index 00000000..b3074b36
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/observe/TestObserveUtil.java
@@ -0,0 +1,53 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.observe;
+
+import java.net.InetSocketAddress;
+
+import org.eclipse.californium.core.coap.CoAP.Type;
+import org.eclipse.californium.core.coap.OptionSet;
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.californium.core.network.serialization.UdpDataSerializer;
+import org.eclipse.californium.elements.AddressEndpointContext;
+import org.eclipse.californium.elements.Connector;
+import org.eclipse.californium.elements.EndpointContext;
+import org.eclipse.californium.elements.RawData;
+import org.eclipse.leshan.core.request.ContentFormat;
+
+public class TestObserveUtil {
+    public static void sendNotification(Connector connector, InetSocketAddress destination, byte[] payload,
+            Response firstCoapResponse, ContentFormat contentFormat) {
+
+        // create observe response
+        Response response = new Response(org.eclipse.californium.core.coap.CoAP.ResponseCode.CONTENT);
+        response.setType(Type.NON);
+        response.setPayload(payload);
+        response.setMID(firstCoapResponse.getMID() + 1);
+        response.setToken(firstCoapResponse.getToken());
+        OptionSet options = new OptionSet().setContentFormat(contentFormat.getCode())
+                .setObserve(firstCoapResponse.getOptions().getObserve() + 1);
+        response.setOptions(options);
+        EndpointContext context = new AddressEndpointContext(destination);
+        response.setDestinationContext(context);
+
+        // serialize response
+        UdpDataSerializer serializer = new UdpDataSerializer();
+        RawData data = serializer.serializeResponse(response);
+
+        // send it
+        connector.send(data);
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadFailedTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadFailedTest.java
new file mode 100644
index 00000000..730aaa1b
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadFailedTest.java
@@ -0,0 +1,106 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.read;
+
+import static org.eclipse.leshan.core.ResponseCode.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.*;
+
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.leshan.core.request.ReadRequest;
+import org.eclipse.leshan.core.response.ReadResponse;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class ReadFailedTest {
+    public IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void cannot_read_non_readable_resource() throws InterruptedException {
+        // read device reboot resource
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 0, 4));
+
+        // verify result
+        assertEquals(METHOD_NOT_ALLOWED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+
+    @Test
+    public void cannot_read_non_existent_object() throws InterruptedException {
+        // read object "50"
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(50));
+
+        // verify result
+        assertEquals(NOT_FOUND, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+
+    @Test
+    public void cannot_read_non_existent_instance() throws InterruptedException {
+        // read 2nd Device resource
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 1));
+
+        // verify result
+        assertEquals(NOT_FOUND, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+
+    @Test
+    public void cannot_read_non_existent_resource() throws InterruptedException {
+        // read device 50 resource
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 0, 50));
+
+        // verify result
+        assertEquals(NOT_FOUND, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+
+    @Test
+    public void cannot_read_security_resource() throws InterruptedException {
+        // read device 50 resource
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(0, 0, 0));
+
+        // verify result
+        assertEquals(NOT_FOUND, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java
new file mode 100644
index 00000000..cbe471e5
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadMultiValueTest.java
@@ -0,0 +1,120 @@
+/*******************************************************************************
+ * 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 v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Zebra Technologies - initial API and implementation
+ *     Achim Kraus (Bosch Software Innovations GmbH) - add test for read security object
+ *     Achim Kraus (Bosch Software Innovations GmbH) - replace close() with destroy()
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.read;
+
+import static org.eclipse.leshan.core.ResponseCode.CONTENT;
+import static org.eclipse.leshan.integration.tests.TestUtil.assertContentFormat;
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.eclipse.leshan.core.node.LwM2mObject;
+import org.eclipse.leshan.core.node.LwM2mObjectInstance;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.ReadRequest;
+import org.eclipse.leshan.core.response.ReadResponse;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ReadMultiValueTest {
+    protected IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    @Parameters(name = "{0}")
+    public static Collection<?> contentFormats() {
+        return Arrays.asList(new Object[][] { //
+                                { ContentFormat.TLV }, //
+                                { ContentFormat.JSON }, //
+                                { ContentFormat.SENML_JSON } });
+    }
+
+    private ContentFormat contentFormat;
+
+    public ReadMultiValueTest(ContentFormat contentFormat) {
+        this.contentFormat = contentFormat;
+    }
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void can_read_empty_object() throws InterruptedException {
+        // read ACL object
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(contentFormat, 2));
+
+        // verify result
+        assertEquals(CONTENT, response.getCode());
+        assertContentFormat(contentFormat, response);
+
+        LwM2mObject object = (LwM2mObject) response.getContent();
+        assertEquals(2, object.getId());
+        assertTrue(object.getInstances().isEmpty());
+
+    }
+
+    @Test
+    public void can_read_object() throws InterruptedException {
+        // read device object
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(contentFormat, 3));
+
+        // verify result
+        assertEquals(CONTENT, response.getCode());
+        assertContentFormat(contentFormat, response);
+
+        LwM2mObject object = (LwM2mObject) response.getContent();
+        assertEquals(3, object.getId());
+
+        LwM2mObjectInstance instance = object.getInstance(0);
+        assertEquals(0, instance.getId());
+    }
+
+    @Test
+    public void can_read_object_instance() throws InterruptedException {
+        // read device single instance
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, 3, 0));
+
+        // verify result
+        assertEquals(CONTENT, response.getCode());
+        assertContentFormat(contentFormat, response);
+
+        LwM2mObjectInstance instance = (LwM2mObjectInstance) response.getContent();
+        assertEquals(0, instance.getId());
+    }
+
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java
new file mode 100644
index 00000000..05e41ed4
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadOpaqueValueTest.java
@@ -0,0 +1,90 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.read;
+
+import static org.eclipse.leshan.core.ResponseCode.CONTENT;
+import static org.eclipse.leshan.integration.tests.IntegrationTestHelper.*;
+import static org.eclipse.leshan.integration.tests.TestUtil.assertContentFormat;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.ReadRequest;
+import org.eclipse.leshan.core.response.ReadResponse;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ReadOpaqueValueTest {
+    protected IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    @Parameters(name = "{0}")
+    public static Collection<?> contentFormats() {
+        return Arrays.asList(new Object[][] { //
+                                { ContentFormat.OPAQUE }, //
+                                { ContentFormat.TEXT }, //
+                                { ContentFormat.TLV }, //
+                                { ContentFormat.JSON }, //
+                                { ContentFormat.SENML_JSON } });
+    }
+
+    private ContentFormat contentFormat;
+
+    public ReadOpaqueValueTest(ContentFormat contentFormat) {
+        this.contentFormat = contentFormat;
+    }
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void can_read_empty_opaque_resource() throws InterruptedException {
+        // read device model number
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 1, OPAQUE_RESOURCE_ID));
+
+        // verify result
+        assertEquals(CONTENT, response.getCode());
+        assertContentFormat(contentFormat, response);
+
+        LwM2mResource resource = (LwM2mResource) response.getContent();
+        assertEquals(Type.OPAQUE, resource.getType());
+        assertEquals(0, ((byte[]) resource.getValue()).length);
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java
new file mode 100644
index 00000000..ea6a6ac6
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/read/ReadSingleValueTest.java
@@ -0,0 +1,116 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.read;
+
+import static org.eclipse.leshan.core.ResponseCode.*;
+import static org.eclipse.leshan.integration.tests.IntegrationTestHelper.*;
+import static org.eclipse.leshan.integration.tests.TestUtil.assertContentFormat;
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.ReadRequest;
+import org.eclipse.leshan.core.response.ReadResponse;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ReadSingleValueTest {
+    protected IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    @Parameters(name = "{0}")
+    public static Collection<?> contentFormats() {
+        return Arrays.asList(new Object[][] { //
+                                { ContentFormat.TEXT }, //
+                                { ContentFormat.TLV }, //
+                                { ContentFormat.JSON }, //
+                                { ContentFormat.SENML_JSON } });
+    }
+
+    private ContentFormat contentFormat;
+
+    public ReadSingleValueTest(ContentFormat contentFormat) {
+        this.contentFormat = contentFormat;
+    }
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void can_read_resource() throws InterruptedException {
+        // read device model number
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, 3, 0, 1));
+
+        // verify result
+        assertEquals(CONTENT, response.getCode());
+        assertContentFormat(contentFormat, response);
+
+        LwM2mResource resource = (LwM2mResource) response.getContent();
+        assertEquals(1, resource.getId());
+        assertEquals(IntegrationTestHelper.MODEL_NUMBER, resource.getValue());
+
+    }
+
+    @Test
+    public void can_read_resource_instance() throws InterruptedException {
+        // read device model number
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, STRING_RESOURCE_INSTANCE_ID, 0), 1000000);
+
+        // verify result
+        assertEquals(CONTENT, response.getCode());
+        assertContentFormat(contentFormat, response);
+
+        LwM2mResourceInstance resourceInstance = (LwM2mResourceInstance) response.getContent();
+        assertEquals(0, resourceInstance.getId());
+        assertEquals(MULTI_INSTANCE, resourceInstance.getValue());
+    }
+
+    @Test
+    public void cannot_read_non_multiple_resource_instance() throws InterruptedException {
+        // read device model number
+        ReadResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, INTEGER_RESOURCE_ID, 0), 1000000);
+
+        // verify result
+        assertEquals(BAD_REQUEST, response.getCode());
+        assertEquals("invalid path : resource is not multiple", response.getErrorMessage());
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteFailedTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteFailedTest.java
new file mode 100644
index 00000000..5b2170a0
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteFailedTest.java
@@ -0,0 +1,93 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.write;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.*;
+
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.leshan.core.ResponseCode;
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.request.WriteRequest;
+import org.eclipse.leshan.core.request.WriteRequest.Mode;
+import org.eclipse.leshan.core.response.WriteResponse;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+public class WriteFailedTest {
+    private IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void cannot_write_non_writable_resource() throws InterruptedException {
+        // try to write unwritable resource like manufacturer on device
+        String manufacturer = "new manufacturer";
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(3, 0, 0, manufacturer));
+
+        // verify result
+        assertEquals(ResponseCode.METHOD_NOT_ALLOWED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+
+    @Test
+    public void cannot_write_security_resource() throws InterruptedException {
+        // try to write unwritable resource like manufacturer on device
+        String uri = "new.dest.server";
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(), new WriteRequest(0, 0, 0, uri));
+
+        // verify result
+        assertEquals(ResponseCode.NOT_FOUND, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+
+    @Test
+    public void cannot_write_replacing_incomplete_object_instance() throws InterruptedException {
+        // write server object
+        LwM2mResource lifetime = LwM2mSingleResource.newIntegerResource(1, 120);
+        LwM2mResource defaultMinPeriod = LwM2mSingleResource.newIntegerResource(2, 10);
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(Mode.REPLACE, 1, 0, lifetime, defaultMinPeriod));
+
+        // verify result
+        assertEquals(ResponseCode.BAD_REQUEST, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java
new file mode 100644
index 00000000..c2f6b342
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteMultiValueTest.java
@@ -0,0 +1,210 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.write;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.leshan.core.ResponseCode;
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.node.LwM2mMultipleResource;
+import org.eclipse.leshan.core.node.LwM2mObjectInstance;
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.ObjectLink;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.ReadRequest;
+import org.eclipse.leshan.core.request.WriteRequest;
+import org.eclipse.leshan.core.request.WriteRequest.Mode;
+import org.eclipse.leshan.core.response.ReadResponse;
+import org.eclipse.leshan.core.response.WriteResponse;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class WriteMultiValueTest {
+    protected IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    @Parameters(name = "{0}")
+    public static Collection<?> contentFormats() {
+        return Arrays.asList(new Object[][] { //
+                                { ContentFormat.TLV }, //
+                                { ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE) }, //
+                                { ContentFormat.JSON }, //
+                                { ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE) }, //
+                                { ContentFormat.SENML_JSON } });
+    }
+
+    private ContentFormat contentFormat;
+
+    public WriteMultiValueTest(ContentFormat contentFormat) {
+        this.contentFormat = contentFormat;
+    }
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void can_write_object_instance() throws InterruptedException {
+        // write device timezone and offset
+        LwM2mResource utcOffset = LwM2mSingleResource.newStringResource(14, "+02");
+        LwM2mResource timeZone = LwM2mSingleResource.newStringResource(15, "Europe/Paris");
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(Mode.REPLACE, contentFormat, 3, 0, utcOffset, timeZone));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read the timezone to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(3, 0));
+        LwM2mObjectInstance instance = (LwM2mObjectInstance) readResponse.getContent();
+        assertEquals(utcOffset, instance.getResource(14));
+        assertEquals(timeZone, instance.getResource(15));
+    }
+
+    @Test
+    public void can_write_replacing_object_instance() throws InterruptedException {
+        // setup server object
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, 1, 0, 3, 60));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // write server object
+        LwM2mResource lifetime = LwM2mSingleResource.newIntegerResource(1, 120);
+        LwM2mResource defaultMinPeriod = LwM2mSingleResource.newIntegerResource(2, 10);
+        LwM2mResource notificationStoring = LwM2mSingleResource.newBooleanResource(6, false);
+        LwM2mResource binding = LwM2mSingleResource.newStringResource(7, "U");
+        response = helper.server.send(helper.getCurrentRegistration(), new WriteRequest(Mode.REPLACE, contentFormat, 1,
+                0, lifetime, defaultMinPeriod, notificationStoring, binding));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read the values to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(1, 0));
+        LwM2mObjectInstance instance = (LwM2mObjectInstance) readResponse.getContent();
+        assertEquals(lifetime, instance.getResource(1));
+        assertEquals(defaultMinPeriod, instance.getResource(2));
+        assertEquals(notificationStoring, instance.getResource(6));
+        assertEquals(binding, instance.getResource(7));
+        assertNull(instance.getResource(3)); // removed not contained optional writable resource
+    }
+
+    @Test
+    public void can_write_updating_object_instance() throws InterruptedException {
+        // setup server object
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, 1, 0, 3, 60));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+        // write server object
+        LwM2mResource lifetime = LwM2mSingleResource.newIntegerResource(1, 120);
+        LwM2mResource defaultMinPeriod = LwM2mSingleResource.newIntegerResource(2, 10);
+        response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(Mode.UPDATE, contentFormat, 1, 0, lifetime, defaultMinPeriod));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read the values to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(1, 0));
+        LwM2mObjectInstance instance = (LwM2mObjectInstance) readResponse.getContent();
+        assertEquals(lifetime, instance.getResource(1));
+        assertEquals(defaultMinPeriod, instance.getResource(2));
+        // no resources are removed when updating
+        assertNotNull(instance.getResource(3));
+        assertNotNull(instance.getResource(6));
+        assertNotNull(instance.getResource(7));
+    }
+
+    @Test
+    public void can_write_multi_instance_objlnk_resource_in_tlv() throws InterruptedException {
+        // object link not yet implemented for some content format.
+        switch (contentFormat.getCode()) {
+        case ContentFormat.JSON_CODE:
+        case ContentFormat.OLD_JSON_CODE:
+            return;
+        }
+
+        Map<Integer, ObjectLink> neighbourCellReport = new HashMap<>();
+        neighbourCellReport.put(0, new ObjectLink(10245, 1));
+        neighbourCellReport.put(1, new ObjectLink(10242, 2));
+        neighbourCellReport.put(2, new ObjectLink(10244, 3));
+
+        // Write objlnk resource in TLV format
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, IntegrationTestHelper.TEST_OBJECT_ID, 0,
+                        IntegrationTestHelper.OBJLNK_MULTI_INSTANCE_RESOURCE_ID, neighbourCellReport, Type.OBJLNK));
+
+        // Verify Write result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // Reading back the written OBJLNK value
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(
+                IntegrationTestHelper.TEST_OBJECT_ID, 0, IntegrationTestHelper.OBJLNK_MULTI_INSTANCE_RESOURCE_ID));
+        LwM2mMultipleResource resource = (LwM2mMultipleResource) readResponse.getContent();
+
+        // verify read value
+        assertEquals(((ObjectLink) resource.getValue(0)).getObjectId(), 10245);
+        assertEquals(((ObjectLink) resource.getValue(0)).getObjectInstanceId(), 1);
+        assertEquals(((ObjectLink) resource.getValue(1)).getObjectId(), 10242);
+        assertEquals(((ObjectLink) resource.getValue(1)).getObjectInstanceId(), 2);
+        assertEquals(((ObjectLink) resource.getValue(2)).getObjectId(), 10244);
+        assertEquals(((ObjectLink) resource.getValue(2)).getObjectInstanceId(), 3);
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java
new file mode 100644
index 00000000..60bcaab1
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteOpaqueValueTest.java
@@ -0,0 +1,100 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.write;
+
+import static org.eclipse.leshan.integration.tests.IntegrationTestHelper.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.leshan.core.ResponseCode;
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.ReadRequest;
+import org.eclipse.leshan.core.request.WriteRequest;
+import org.eclipse.leshan.core.response.ReadResponse;
+import org.eclipse.leshan.core.response.WriteResponse;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class WriteOpaqueValueTest {
+    protected IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    @Parameters(name = "{0}")
+    public static Collection<?> contentFormats() {
+        return Arrays.asList(new Object[][] { //
+                                { ContentFormat.OPAQUE }, //
+                                { ContentFormat.TEXT }, //
+                                { ContentFormat.TLV }, //
+                                { ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE) }, //
+                                { ContentFormat.JSON }, //
+                                { ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE) }, //
+                                { ContentFormat.SENML_JSON } });
+    }
+
+    private ContentFormat contentFormat;
+
+    public WriteOpaqueValueTest(ContentFormat contentFormat) {
+        this.contentFormat = contentFormat;
+    }
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void write_opaque_resource() throws InterruptedException {
+        // write resource
+        byte[] expectedvalue = new byte[] { 1, 2, 3 };
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, TEST_OBJECT_ID, 0, OPAQUE_RESOURCE_ID, expectedvalue));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read resource to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, OPAQUE_RESOURCE_ID));
+        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
+        assertArrayEquals(expectedvalue, (byte[]) resource.getValue());
+    }
+}
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java
new file mode 100644
index 00000000..5c9330ca
--- /dev/null
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/write/WriteSingleValueTest.java
@@ -0,0 +1,286 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.integration.tests.write;
+
+import static org.eclipse.leshan.core.ResponseCode.CHANGED;
+import static org.eclipse.leshan.integration.tests.IntegrationTestHelper.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.*;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Date;
+
+import org.eclipse.californium.core.coap.Response;
+import org.eclipse.leshan.core.ResponseCode;
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
+import org.eclipse.leshan.core.node.LwM2mSingleResource;
+import org.eclipse.leshan.core.node.ObjectLink;
+import org.eclipse.leshan.core.node.codec.CodecException;
+import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.core.request.ReadRequest;
+import org.eclipse.leshan.core.request.WriteRequest;
+import org.eclipse.leshan.core.response.ErrorCallback;
+import org.eclipse.leshan.core.response.ReadResponse;
+import org.eclipse.leshan.core.response.ResponseCallback;
+import org.eclipse.leshan.core.response.WriteResponse;
+import org.eclipse.leshan.core.util.datatype.ULong;
+import org.eclipse.leshan.integration.tests.IntegrationTestHelper;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class WriteSingleValueTest {
+    protected IntegrationTestHelper helper = new IntegrationTestHelper();
+
+    @Parameters(name = "{0}")
+    public static Collection<?> contentFormats() {
+        return Arrays.asList(new Object[][] { //
+                                { ContentFormat.TEXT }, //
+                                { ContentFormat.TLV }, //
+                                { ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE) }, //
+                                { ContentFormat.JSON }, //
+                                { ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE) }, //
+                                { ContentFormat.SENML_JSON } });
+    }
+
+    private ContentFormat contentFormat;
+
+    public WriteSingleValueTest(ContentFormat contentFormat) {
+        this.contentFormat = contentFormat;
+    }
+
+    @Before
+    public void start() {
+        helper.initialize();
+        helper.createServer();
+        helper.server.start();
+        helper.createClient();
+        helper.client.start();
+        helper.waitForRegistrationAtServerSide(1);
+    }
+
+    @After
+    public void stop() {
+        helper.client.destroy(false);
+        helper.server.destroy();
+        helper.dispose();
+    }
+
+    @Test
+    public void write_string_resource() throws InterruptedException {
+        // write resource
+        String expectedvalue = "stringvalue";
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, TEST_OBJECT_ID, 0, STRING_RESOURCE_ID, expectedvalue));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read resource to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, STRING_RESOURCE_ID));
+        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
+        assertEquals(expectedvalue, resource.getValue());
+    }
+
+    @Test
+    public void write_boolean_resource() throws InterruptedException {
+        // write resource
+        boolean expectedvalue = true;
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, TEST_OBJECT_ID, 0, BOOLEAN_RESOURCE_ID, expectedvalue));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read resource to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, BOOLEAN_RESOURCE_ID));
+        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
+        assertEquals(expectedvalue, resource.getValue());
+    }
+
+    @Test
+    public void write_integer_resource() throws InterruptedException {
+        // write resource
+        long expectedvalue = 999l;
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, TEST_OBJECT_ID, 0, INTEGER_RESOURCE_ID, expectedvalue));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read resource to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, INTEGER_RESOURCE_ID));
+        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
+        assertEquals(expectedvalue, resource.getValue());
+    }
+
+    @Test
+    public void can_write_string_resource_instance() throws InterruptedException {
+        write_string_resource_instance(contentFormat, 0);
+        write_string_resource_instance(contentFormat, 1);
+    }
+
+    private void write_string_resource_instance(ContentFormat format, int resourceInstance)
+            throws InterruptedException {
+        // read device model number
+        String valueToWrite = "newValue";
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(), new WriteRequest(format,
+                TEST_OBJECT_ID, 0, STRING_RESOURCE_INSTANCE_ID, resourceInstance, valueToWrite, Type.STRING));
+
+        // verify result
+        assertEquals(CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read resource to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(format, TEST_OBJECT_ID, 0, STRING_RESOURCE_INSTANCE_ID, resourceInstance));
+
+        // verify result
+        LwM2mResourceInstance resource = (LwM2mResourceInstance) readResponse.getContent();
+        assertEquals(valueToWrite, resource.getValue());
+    }
+
+    @Test
+    public void write_float_resource() throws InterruptedException {
+        // write resource
+        double expectedvalue = 999.99;
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, TEST_OBJECT_ID, 0, FLOAT_RESOURCE_ID, expectedvalue));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read resource to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, FLOAT_RESOURCE_ID));
+        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
+        assertEquals(expectedvalue, resource.getValue());
+    }
+
+    @Test
+    public void write_time_resource() throws InterruptedException {
+        // write resource
+        Date expectedvalue = new Date(946681000l); // second accuracy
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, TEST_OBJECT_ID, 0, TIME_RESOURCE_ID, expectedvalue));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read resource to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(contentFormat, TEST_OBJECT_ID, 0, TIME_RESOURCE_ID));
+        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
+        assertEquals(expectedvalue, resource.getValue());
+    }
+
+    @Test
+    public void write_unsigned_integer_resource() throws InterruptedException {
+        // write resource
+        ULong expectedvalue = ULong.valueOf("18446744073709551615"); // this unsigned integer can not be stored in a
+
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, TEST_OBJECT_ID, 0, UNSIGNED_INTEGER_RESOURCE_ID, expectedvalue));
+
+        // verify result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // read resource to check the value changed
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
+                new ReadRequest(TEST_OBJECT_ID, 0, UNSIGNED_INTEGER_RESOURCE_ID));
+        LwM2mResource resource = (LwM2mResource) readResponse.getContent();
+        assertEquals(expectedvalue, resource.getValue());
+    }
+
+    @Test
+    public void can_write_single_instance_objlnk_resource() throws InterruptedException {
+
+        // object link not yet implemented for some content format.
+        switch (contentFormat.getCode()) {
+        case ContentFormat.JSON_CODE:
+        case ContentFormat.OLD_JSON_CODE:
+            return;
+        }
+
+        ObjectLink data = new ObjectLink(10245, 1);
+
+        // Write objlnk resource
+        WriteResponse response = helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, IntegrationTestHelper.TEST_OBJECT_ID, 0,
+                        IntegrationTestHelper.OBJLNK_SINGLE_INSTANCE_RESOURCE_ID, data));
+
+        // Verify Write result
+        assertEquals(ResponseCode.CHANGED, response.getCode());
+        assertNotNull(response.getCoapResponse());
+        assertThat(response.getCoapResponse(), is(instanceOf(Response.class)));
+
+        // Reading back the written OBJLNK value
+        ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(), new ReadRequest(
+                IntegrationTestHelper.TEST_OBJECT_ID, 0, IntegrationTestHelper.OBJLNK_SINGLE_INSTANCE_RESOURCE_ID));
+        LwM2mSingleResource resource = (LwM2mSingleResource) readResponse.getContent();
+
+        // verify read value
+        assertEquals(((ObjectLink) resource.getValue()).getObjectId(), 10245);
+        assertEquals(((ObjectLink) resource.getValue()).getObjectInstanceId(), 1);
+    }
+
+    @Test(expected = CodecException.class)
+    public void send_writerequest_synchronously_with_bad_payload_raises_codeexception() throws InterruptedException {
+        helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, 3, 0, 13, "a string instead of timestamp for currenttime resource"));
+
+    }
+
+    @Test(expected = CodecException.class)
+    public void send_writerequest_asynchronously_with_bad_payload_raises_codeexception() throws InterruptedException {
+        helper.server.send(helper.getCurrentRegistration(),
+                new WriteRequest(contentFormat, 3, 0, 13, "a string instead of timestamp for currenttime resource"),
+                new ResponseCallback<WriteResponse>() {
+                    @Override
+                    public void onResponse(WriteResponse response) {
+                    }
+                }, new ErrorCallback() {
+                    @Override
+                    public void onError(Exception e) {
+                    }
+                });
+    }
+}