Repository: eclipse/leshan
Pull request #877 (2020-08-21 13:17:51)

Add resources instances operations
**In LWM2M 1.1,  operation on resource instances has been added.**

This concerns **requests** : 
 - read
 - write
 - observe and cancel observe
-  write attributes

and should impact all **decoder/encoder** : 
- TLV
- text
- opaque
- JSON
- senML+json and cbor (which are in a work in progress state)

The first commits aims to prepare the work to introduce this new feature and implement the read request with TLV content format.

(This PR is a work in progress.)

zakharenkodmytro: Hi @sbernard31 , I'm looking in to it right now. 
sbernard31: @zakharenkodmytro, I integrated your work.
And push some modifications : 

- 367e7ad : Fix some formatting issue. 
- 1ce423c : Remove usage deprecated getValues()  => we don't need it anymore
- 79ccfe6 : Add write tests about resource instances operation for tlv.  => also did some naming for consistency.
- e60594d : LwM2mInstanceEnabler.write should handle LwM2mResourceInstance  => I change value type from `Object` to `LwM2mResourceInstance`, it sounds more consistent with other write methods.
- bf39c92 :  Add default impl for write resource instance in BaseInstanceEnabler  => I remove some default implementations from SimpleInstanceEnabler to make sure that default BaseInstanceEnable implementation works and is called by unit tests. (not sure if it's a good idea, it could come back later)
zakharenkodmytro: > * bf39c92

@sbernard31 , all looks good to me.
zakharenkodmytro: > @zakharenkodmytro, I integrated your work.
> And push some modifications :
> 
> * [367e7ad](https://github.com/eclipse/leshan/commit/367e7ad1bb7b4c407ae305f8b2482458644f1fd8) : Fix some formatting issue.
> * [1ce423c](https://github.com/eclipse/leshan/commit/1ce423c9ba6dc0522934c0bcca03fa99e0d83288) : Remove usage deprecated getValues()  => we don't need it anymore
> * [79ccfe6](https://github.com/eclipse/leshan/commit/79ccfe6c7cd8581730e1d8481689d1d6a7a87167) : Add write tests about resource instances operation for tlv.  => also did some naming for consistency.
> * [e60594d](https://github.com/eclipse/leshan/commit/e60594d082f00cfe11041bbcb629a70aaa001eb0) : LwM2mInstanceEnabler.write should handle LwM2mResourceInstance  => I change value type from `Object` to `LwM2mResourceInstance`, it sounds more consistent with other write methods.
> * [bf39c92](https://github.com/eclipse/leshan/commit/bf39c92c4d137d229bae737bf5c0e661856b4582) :  Add default impl for write resource instance in BaseInstanceEnabler  => I remove some default implementations from SimpleInstanceEnabler to make sure that default BaseInstanceEnable implementation works and is called by unit tests. (not sure if it's a good idea, it could come back later)

@sbernard31 , looks good to me. It always good to have second look and some modifications before merge to master.

Patch:
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseInstanceEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseInstanceEnabler.java
index d642d8ee..ba382b96 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseInstanceEnabler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseInstanceEnabler.java
@@ -27,8 +27,11 @@ import org.eclipse.leshan.client.LwM2mClient;
 import org.eclipse.leshan.client.servers.ServerIdentity;
 import org.eclipse.leshan.core.model.ObjectModel;
 import org.eclipse.leshan.core.model.ResourceModel;
+import org.eclipse.leshan.core.node.LwM2mMultipleResource;
+import org.eclipse.leshan.core.node.LwM2mNode;
 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.response.ExecuteResponse;
 import org.eclipse.leshan.core.response.ObserveResponse;
 import org.eclipse.leshan.core.response.ReadResponse;
@@ -125,6 +128,20 @@ public class BaseInstanceEnabler implements LwM2mInstanceEnabler {
         return ReadResponse.notFound();
     }
 
+    @Override
+    public ReadResponse read(ServerIdentity identity, int resourceId, int resourceInstance) {
+        ReadResponse response = read(identity, resourceId);
+        if (response.isFailure())
+            return response;
+        LwM2mMultipleResource resource = (LwM2mMultipleResource) response.getContent();
+        LwM2mResourceInstance resourceIn = resource.getInstance(resourceInstance);
+        if (resourceIn == null) {
+            return ReadResponse.notFound();
+        } else {
+            return ReadResponse.success(resourceIn);
+        }
+    }
+
     @Override
     public WriteResponse write(ServerIdentity identity, boolean replace, LwM2mObjectInstance value) {
         Map<Integer, LwM2mResource> resourcesToWrite = new HashMap<>(value.getResources());
@@ -154,6 +171,26 @@ public class BaseInstanceEnabler implements LwM2mInstanceEnabler {
         return WriteResponse.notFound();
     }
 
+    @Override
+    public WriteResponse write(ServerIdentity identity, int resourceid, int resourceInstanceId,
+            LwM2mResourceInstance value) {
+        // this is a sub-optimal default implementation
+        ReadResponse response = read(ServerIdentity.SYSTEM, resourceid);
+        if (response.isSuccess()) {
+            LwM2mNode content = response.getContent();
+            if (content instanceof LwM2mMultipleResource) {
+                LwM2mMultipleResource multiresource = ((LwM2mMultipleResource) content);
+                Map<Integer, LwM2mResourceInstance> instances = new HashMap<>(multiresource.getInstances());
+                if (instances.containsKey(resourceInstanceId)) {
+                    instances.put(resourceInstanceId, value);
+                    return write(identity, resourceid,
+                            new LwM2mMultipleResource(resourceInstanceId, value.getType(), instances.values()));
+                }
+            }
+        }
+        return WriteResponse.notFound();
+    }
+
     @Override
     public ExecuteResponse execute(ServerIdentity identity, int resourceid, String params) {
         return ExecuteResponse.notFound();
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java
index c1399262..8f18e509 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/BaseObjectEnabler.java
@@ -158,12 +158,14 @@ public abstract class BaseObjectEnabler implements LwM2mObjectEnabler {
             }
 
             // check if the resource is readable.
-            if (path.isResource()) {
+            if (path.isResource() || path.isResourceInstance()) {
                 ResourceModel resourceModel = objectModel.resources.get(path.getResourceId());
                 if (resourceModel == null) {
                     return ReadResponse.notFound();
                 } else if (!resourceModel.operations.isReadable()) {
                     return ReadResponse.methodNotAllowed();
+                } else if (path.isResourceInstance() && !resourceModel.multiple) {
+                    return ReadResponse.badRequest("invalid path : resource is not multiple");
                 }
             }
         }
@@ -195,7 +197,7 @@ public abstract class BaseObjectEnabler implements LwM2mObjectEnabler {
                 return WriteResponse.notFound();
             }
 
-            if (path.isResource()) {
+            if (path.isResource() || path.isResourceInstance()) {
                 // resource write:
                 // check if the resource is writable
                 if (LwM2mId.SECURITY != id) { // security resources are writable by SYSTEM
@@ -204,6 +206,8 @@ public abstract class BaseObjectEnabler implements LwM2mObjectEnabler {
                         return WriteResponse.notFound();
                     } else if (!resourceModel.operations.isWritable()) {
                         return WriteResponse.methodNotAllowed();
+                    } else if (path.isResourceInstance() && !resourceModel.multiple) {
+                        return WriteResponse.badRequest("invalid path : resource is not multiple");
                     }
                 }
             } else if (path.isObjectInstance()) {
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/DummyInstanceEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/DummyInstanceEnabler.java
index 31542186..587203bc 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/DummyInstanceEnabler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/DummyInstanceEnabler.java
@@ -27,6 +27,7 @@ import org.eclipse.leshan.core.model.ObjectModel;
 import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.node.LwM2mMultipleResource;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.response.ExecuteResponse;
 import org.eclipse.leshan.core.response.ReadResponse;
 import org.eclipse.leshan.core.response.WriteResponse;
@@ -56,12 +57,27 @@ public class DummyInstanceEnabler extends SimpleInstanceEnabler {
         return super.read(identity, resourceid);
     }
 
+    @Override
+    public ReadResponse read(ServerIdentity identity, int resourceid, int resourceInstance) {
+        LOG.info("Read on {} Resource /{}/{}/{}/{} ", getModel().name, getModel().id, getId(), resourceid,
+                resourceInstance);
+        return super.read(identity, resourceid, resourceInstance);
+    }
+
     @Override
     public WriteResponse write(ServerIdentity identity, int resourceid, LwM2mResource value) {
         LOG.info("Write on {} Resource /{}/{}/{} ", getModel().name, getModel().id, getId(), resourceid);
         return super.write(identity, resourceid, value);
     }
 
+    @Override
+    public WriteResponse write(ServerIdentity identity, int resourceid, int resourceInstance,
+            LwM2mResourceInstance value) {
+        LOG.info("Write on {} Resource  Instance/{}/{}/{}/{} ", getModel().name, getModel().id, getId(), resourceid,
+                resourceInstance);
+        return super.write(identity, resourceid, resourceInstance, value);
+    }
+
     @Override
     public ExecuteResponse execute(ServerIdentity identity, int resourceid, String params) {
         LOG.info("Execute {} Resource /{}/{}/{} with params {}", getModel().name, getModel().id, getId(), resourceid,
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mInstanceEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mInstanceEnabler.java
index 2570fdcf..63f665a2 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mInstanceEnabler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/LwM2mInstanceEnabler.java
@@ -25,6 +25,7 @@ import org.eclipse.leshan.client.servers.ServerIdentity;
 import org.eclipse.leshan.core.model.ObjectModel;
 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.WriteRequest.Mode;
 import org.eclipse.leshan.core.response.ExecuteResponse;
 import org.eclipse.leshan.core.response.ObserveResponse;
@@ -112,6 +113,19 @@ public interface LwM2mInstanceEnabler {
      */
     ReadResponse read(ServerIdentity identity, int resourceId);
 
+    /**
+     * Gets the current value of one of this LWM2M object instance's resources instance.
+     * 
+     * @param identity the identity of the requester. This could be an internal call in this case
+     *        <code> identity == ServerIdentity.SYSTEM</code>.
+     * @param resourceId the ID of the resource to get the value of
+     * @param resourceInstance the ID of the resource instance to get the value of
+     * @return the response object representing the outcome of the operation. An implementation should set the result's
+     *         {@link ReadResponse#getCode() response code} to either reflect the success or reason for failure to
+     *         retrieve the value.
+     */
+    ReadResponse read(ServerIdentity identity, int resourceId, int resourceInstance);
+
     /**
      * Sets all resources of this LWM2M object instance.
      * 
@@ -138,6 +152,20 @@ public interface LwM2mInstanceEnabler {
      */
     WriteResponse write(ServerIdentity identity, int resourceid, LwM2mResource value);
 
+    /**
+     * Sets the value of one of this LWM2M object instance's resources instance.
+     * 
+     * @param identity the identity of the requester. This could be an internal call in this case
+     *        <code> identity == ServerIdentity.SYSTEM</code>.
+     * @param resourceid the ID of the resource to set the value for
+     * @param resourceInstance the ID of the resource instance to set the value of
+     * @param value the value to set the resource instance to
+     * @return the response object representing the outcome of the operation. An implementation should set the result's
+     *         {@link WriteResponse#getCode() response code} to either reflect the success or reason for failure to set
+     *         the value.
+     */
+    WriteResponse write(ServerIdentity identity, int resourceid, int resourceInstance, LwM2mResourceInstance value);
+
     /**
      * Executes the operation represented by one of this LWM2M object instance's resources.
      * 
diff --git a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java
index e0930669..615a75ed 100644
--- a/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java
+++ b/leshan-client-core/src/main/java/org/eclipse/leshan/client/resource/ObjectEnabler.java
@@ -34,6 +34,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.request.BootstrapDeleteRequest;
 import org.eclipse.leshan.core.request.BootstrapWriteRequest;
 import org.eclipse.leshan.core.request.ContentFormat;
@@ -212,7 +213,12 @@ public class ObjectEnabler extends BaseObjectEnabler {
         }
 
         // Manage Resource case
-        return instance.read(identity, path.getResourceId());
+        if (path.getResourceInstanceId() == null) {
+            return instance.read(identity, path.getResourceId());
+        }
+
+        // Manage Resource Instance case
+        return instance.read(identity, path.getResourceId(), path.getResourceInstanceId());
     }
 
     @Override
@@ -258,7 +264,13 @@ public class ObjectEnabler extends BaseObjectEnabler {
         }
 
         // Manage Resource case
-        return instance.write(identity, path.getResourceId(), (LwM2mResource) request.getNode());
+        if (path.getResourceInstanceId() == null) {
+            return instance.write(identity, path.getResourceId(), (LwM2mResource) request.getNode());
+        }
+
+        // Manage Resource Instance case
+        return instance.write(identity, path.getResourceId(), path.getResourceInstanceId(),
+                ((LwM2mResourceInstance) request.getNode()));
     }
 
     @Override
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mMultipleResource.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mMultipleResource.java
index dcaf5a84..ec7e9bd2 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mMultipleResource.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mMultipleResource.java
@@ -15,32 +15,52 @@
  *******************************************************************************/
 package org.eclipse.leshan.core.node;
 
-import java.util.AbstractMap;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
-import java.util.Iterator;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.NoSuchElementException;
-import java.util.Objects;
 
 import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.util.Validate;
 
 /**
  * A resource which contains several resource instances.
  * 
- * A resource instance is defined by a numeric identifier and a value. There are accessible via {@link #getValues()}
+ * A resource instance is defined by a numeric identifier and a value. There are accessible via {@link #getInstances()}
  */
 public class LwM2mMultipleResource implements LwM2mResource {
 
     private final int id;
 
-    private final Map<Integer, Object> values;
+    private final Map<Integer, LwM2mResourceInstance> instances;
 
     private final Type type;
 
+    public LwM2mMultipleResource(int id, Type type, Collection<LwM2mResourceInstance> instances) {
+        Validate.notNull(instances);
+        Validate.notNull(type);
+        LwM2mNodeUtil.validateResourceId(id);
+
+        this.id = id;
+        this.type = type;
+        Map<Integer, LwM2mResourceInstance> instancesMap = new HashMap<>(instances.size());
+        for (LwM2mResourceInstance instance : instances) {
+            if (type != instance.getType())
+                new LwM2mNodeException("Invalid resource instance %d, type is %s but resource is %s.", id,
+                        instance.getType(), type);
+            instancesMap.put(instance.getId(), instance);
+        }
+        this.instances = Collections.unmodifiableMap(instancesMap);
+    }
+
+    public LwM2mMultipleResource(int id, Type type, LwM2mResourceInstance... instances) {
+        this(id, type, Arrays.asList(instances));
+    }
+
     protected LwM2mMultipleResource(int id, Map<Integer, ?> values, Type type) {
         LwM2mNodeUtil.validateNotNull(values, "values MUST NOT be null");
         LwM2mNodeUtil.validateResourceId(id);
@@ -49,7 +69,11 @@ public class LwM2mMultipleResource implements LwM2mResource {
             LwM2mNodeUtil.validateResourceInstanceId(instanceId);
         }
         this.id = id;
-        this.values = Collections.unmodifiableMap(new HashMap<>(values));
+        Map<Integer, LwM2mResourceInstance> val = new HashMap<>();
+        for (Entry<Integer, ?> entry : values.entrySet()) {
+            val.put(entry.getKey(), LwM2mResourceInstance.newInstance(entry.getKey(), entry.getValue(), type));
+        }
+        this.instances = Collections.unmodifiableMap(val);
         this.type = type;
     }
 
@@ -135,7 +159,8 @@ public class LwM2mMultipleResource implements LwM2mResource {
     }
 
     /**
-     * @exception NoSuchElementException use {@link #getValue(int)} or {@link #getValue(int)} instead.
+     * @exception NoSuchElementException use {@link #getValue(int)} or {@link #getInstances()} or
+     *            {@link #getInstance(int)} instead.
      */
     @Override
     public Object getValue() {
@@ -146,16 +171,27 @@ public class LwM2mMultipleResource implements LwM2mResource {
      * {@inheritDoc}
      */
     @Override
-    public Map<Integer, ?> getValues() {
-        return values;
+    public Object getValue(int id) {
+        LwM2mResourceInstance resourceInstance = instances.get(id);
+        if (resourceInstance != null)
+            return resourceInstance.getValue();
+        return null;
     }
 
     /**
      * {@inheritDoc}
      */
     @Override
-    public Object getValue(int id) {
-        return values.get(id);
+    public LwM2mResourceInstance getInstance(int id) {
+        return instances.get(id);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public Map<Integer, LwM2mResourceInstance> getInstances() {
+        return instances;
     }
 
     /**
@@ -177,31 +213,10 @@ public class LwM2mMultipleResource implements LwM2mResource {
         int result = 1;
         result = prime * result + id;
         result = prime * result + ((type == null) ? 0 : type.hashCode());
-        result = prime * result + ((values == null) ? 0 : internalHashCode(values));
+        result = prime * result + ((instances == null) ? 0 : instances.hashCode());
         return result;
     }
 
-    /**
-     * This is a copy of {@link AbstractMap#hashCode()} with custom code to handle byte array equality
-     */
-    private int internalHashCode(Map<?, ?> m) {
-        int h = 0;
-        Iterator<?> i = m.entrySet().iterator();
-        if (type == Type.OPAQUE) {
-            // Custom hashcode to handle byte arrays
-            while (i.hasNext()) {
-                Entry<?, ?> e = (Entry<?, ?>) i.next();
-                h += Objects.hashCode(e.getKey()) ^ Arrays.hashCode((byte[]) e.getValue());
-            }
-        } else {
-            while (i.hasNext()) {
-                Entry<?, ?> e = (Entry<?, ?>) i.next();
-                h += Objects.hashCode(e.getKey()) ^ Objects.hashCode(e.getValue());
-            }
-        }
-        return h;
-    }
-
     @Override
     public boolean equals(Object obj) {
         if (this == obj)
@@ -215,73 +230,17 @@ public class LwM2mMultipleResource implements LwM2mResource {
             return false;
         if (type != other.type)
             return false;
-        if (values == null) {
-            if (other.values != null)
+        if (instances == null) {
+            if (other.instances != null)
                 return false;
-            // Custom equals to handle byte arrays
-        } else if (!internalMapEquals(values, other.values))
-            return false;
-        return true;
-    }
-
-    /**
-     * This is a copy of {@link AbstractMap#equals(Object)} with custom code to handle byte array equality
-     */
-    private boolean internalMapEquals(Map<?, ?> m1, Object o2) {
-        if (o2 == this)
-            return true;
-
-        if (!(o2 instanceof Map))
+        } else if (!instances.equals(other.instances))
             return false;
-        Map<?, ?> m2 = (Map<?, ?>) o2;
-        if (m2.size() != m1.size())
-            return false;
-
-        try {
-            for (Object o : m1.entrySet()) {
-                Entry<?, ?> e = (Entry<?, ?>) o;
-                Object key = e.getKey();
-                Object value = e.getValue();
-                if (value == null) {
-                    if (!(m2.get(key) == null && m2.containsKey(key)))
-                        return false;
-                } else {
-                    // Custom equals to handle byte arrays
-                    return type == Type.OPAQUE ? Arrays.equals((byte[]) value, (byte[]) m2.get(key))
-                            : value.equals(m2.get(key));
-                }
-            }
-        } catch (ClassCastException | NullPointerException unused) {
-            return false;
-        }
-
         return true;
     }
 
     @Override
     public String toString() {
-        Object printableValue;
-        if (type == Type.OPAQUE) {
-            // We don't print OPAQUE value as this could be credentials one.
-            // Not ideal but didn't find better way for now.
-            StringBuilder sb = new StringBuilder();
-            sb.append("{");
-            Iterator<Entry<Integer, Object>> iter = values.entrySet().iterator();
-            while (iter.hasNext()) {
-                Entry<Integer, Object> entry = iter.next();
-                sb.append(entry.getKey());
-                sb.append("=");
-                sb.append(((byte[]) entry.getValue()).length + "Bytes");
-                if (iter.hasNext()) {
-                    sb.append(", ");
-                }
-            }
-            sb.append("}");
-            printableValue = sb.toString();
-        } else {
-            printableValue = values;
-        }
-        return String.format("LwM2mMultipleResource [id=%s, values=%s, type=%s]", id, printableValue, type);
+        return String.format("LwM2mMultipleResource [id=%s, values=%s, type=%s]", id, instances, type);
     }
 
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeVisitor.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeVisitor.java
index 179e91a2..cb136e74 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeVisitor.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mNodeVisitor.java
@@ -26,4 +26,6 @@ public interface LwM2mNodeVisitor {
 
     void visit(LwM2mResource resource);
 
+    void visit(LwM2mResourceInstance instance);
+
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mResource.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mResource.java
index 0c85999a..7c50cc6b 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mResource.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mResource.java
@@ -57,7 +57,7 @@ public interface LwM2mResource extends LwM2mNode {
     /**
      * This method is only available if {@link #isMultiInstances()} return <code>true</code>.
      * 
-     * The type of the right part of the returned map depends on the {@link #getType()} method.
+     * The type of the returned value depends on the {@link #getType()} method.
      * 
      * If {@link #getType()} returns {@link Type#BOOLEAN}, the value is a {@link Boolean}.<br>
      * If {@link #getType()} returns {@link Type#STRING}, the value is a {@link String}.<br>
@@ -66,23 +66,21 @@ public interface LwM2mResource extends LwM2mNode {
      * If {@link #getType()} returns {@link Type#INTEGER}, the value is a {@link Long}.<br>
      * If {@link #getType()} returns {@link Type#FLOAT}, the value is a {@link Double}.<br>
      * 
-     * @return the values of each resource instances (key is the resource instance identifier).
+     * @return the value a resource instance with the given identifier.
      */
-    Map<Integer, ?> getValues();
+    Object getValue(int id);
 
     /**
      * This method is only available if {@link #isMultiInstances()} return <code>true</code>.
      * 
-     * The type of the returned value depends on the {@link #getType()} method.
-     * 
-     * If {@link #getType()} returns {@link Type#BOOLEAN}, the value is a {@link Boolean}.<br>
-     * If {@link #getType()} returns {@link Type#STRING}, the value is a {@link String}.<br>
-     * If {@link #getType()} returns {@link Type#OPAQUE}, the value is a byte array.<br>
-     * If {@link #getType()} returns {@link Type#TIME}, the value is a {@link Date}.<br>
-     * If {@link #getType()} returns {@link Type#INTEGER}, the value is a {@link Long}.<br>
-     * If {@link #getType()} returns {@link Type#FLOAT}, the value is a {@link Double}.<br>
+     * @return the resource instance with the given identifier.
+     */
+    LwM2mResourceInstance getInstance(int id);
+
+    /**
+     * This method is only available if {@link #isMultiInstances()} return <code>true</code>.
      * 
-     * @return the value a resource instance with the given identifier.
+     * @return all resource instances.
      */
-    Object getValue(int id);
+    Map<Integer, LwM2mResourceInstance> getInstances();
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mResourceInstance.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mResourceInstance.java
new file mode 100644
index 00000000..c378561c
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mResourceInstance.java
@@ -0,0 +1,170 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Sierra Wireless and others.
+ * 
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v2.0
+ * and Eclipse Distribution License v1.0 which accompany this distribution.
+ * 
+ * The Eclipse Public License is available at
+ *    http://www.eclipse.org/legal/epl-v20.html
+ * and the Eclipse Distribution License is available at
+ *    http://www.eclipse.org/org/documents/edl-v10.html.
+ * 
+ * Contributors:
+ *     Sierra Wireless - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.leshan.core.node;
+
+import java.util.Arrays;
+import java.util.Date;
+
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+
+/**
+ * An instance of {@link LwM2mMultipleResource}.
+ */
+public class LwM2mResourceInstance implements LwM2mNode {
+
+    private final int id;
+    private final Object value;
+    private final Type type;
+
+    protected LwM2mResourceInstance(int id, Object value, Type type) {
+        LwM2mNodeUtil.validateNotNull(value, "value MUST NOT be null");
+        LwM2mNodeUtil.validateResourceInstanceId(id);
+
+        this.id = id;
+        this.value = value;
+        this.type = type;
+    }
+
+    public static LwM2mResourceInstance newInstance(int id, Object value, Type type) {
+        String doesNotMatchMessage = "Value does not match the given datatype";
+        switch (type) {
+        case INTEGER:
+            if (!(value instanceof Long))
+                throw new LwM2mNodeException(doesNotMatchMessage);
+            break;
+        case FLOAT:
+            if (!(value instanceof Double))
+                throw new LwM2mNodeException(doesNotMatchMessage);
+            break;
+        case BOOLEAN:
+            if (!(value instanceof Boolean))
+                throw new LwM2mNodeException(doesNotMatchMessage);
+            break;
+        case OPAQUE:
+            if (!(value instanceof byte[]))
+                throw new LwM2mNodeException(doesNotMatchMessage);
+            break;
+        case STRING:
+            if (!(value instanceof String))
+                throw new LwM2mNodeException(doesNotMatchMessage);
+            break;
+        case TIME:
+            if (!(value instanceof Date))
+                throw new LwM2mNodeException(doesNotMatchMessage);
+            break;
+        case OBJLNK:
+            if (!(value instanceof ObjectLink))
+                throw new LwM2mNodeException(doesNotMatchMessage);
+            break;
+        default:
+            throw new LwM2mNodeException(String.format("Type %s is not supported", type.name()));
+        }
+        return new LwM2mResourceInstance(id, value, type);
+    }
+
+    public static LwM2mResourceInstance newStringInstance(int id, String value) {
+        return new LwM2mResourceInstance(id, value, Type.STRING);
+    }
+
+    public static LwM2mResourceInstance newIntegerInstance(int id, long value) {
+        return new LwM2mResourceInstance(id, value, Type.INTEGER);
+    }
+
+    public static LwM2mResourceInstance newObjectLinkInstance(int id, ObjectLink objlink) {
+        return new LwM2mResourceInstance(id, objlink, Type.OBJLNK);
+    }
+
+    public static LwM2mResourceInstance newBooleanInstance(int id, boolean value) {
+        return new LwM2mResourceInstance(id, value, Type.BOOLEAN);
+    }
+
+    public static LwM2mResourceInstance newFloatInstance(int id, double value) {
+        return new LwM2mResourceInstance(id, value, Type.FLOAT);
+    }
+
+    public static LwM2mResourceInstance newDateInstance(int id, Date value) {
+        return new LwM2mResourceInstance(id, value, Type.TIME);
+    }
+
+    public static LwM2mResourceInstance newBinaryInstance(int id, byte[] value) {
+        return new LwM2mResourceInstance(id, value, Type.OPAQUE);
+    }
+
+    @Override
+    public int getId() {
+        return id;
+    }
+
+    public Object getValue() {
+        return value;
+    }
+
+    public Type getType() {
+        return type;
+    }
+
+    @Override
+    public void accept(LwM2mNodeVisitor visitor) {
+        visitor.visit(this);
+    }
+
+    @Override
+    public int hashCode() {
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + id;
+        result = prime * result + ((type == null) ? 0 : type.hashCode());
+        if (type == Type.OPAQUE) {
+            // Custom hashcode to handle byte arrays
+            result = prime * result + ((value == null) ? 0 : Arrays.hashCode((byte[]) value));
+        } else {
+            result = prime * result + ((value == null) ? 0 : value.hashCode());
+        }
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj)
+            return true;
+        if (obj == null)
+            return false;
+        if (getClass() != obj.getClass())
+            return false;
+        LwM2mResourceInstance other = (LwM2mResourceInstance) obj;
+        if (id != other.id)
+            return false;
+        if (type != other.type)
+            return false;
+        if (value == null) {
+            if (other.value != null)
+                return false;
+        } else {
+            // Custom equals to handle byte arrays
+            return type == Type.OPAQUE ? Arrays.equals((byte[]) value, (byte[]) other.value)
+                    : value.equals(other.value);
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        // We don't print OPAQUE value as this could be credentials one.
+        // Not ideal but didn't find better way for now.
+        return String.format("LwM2mResourceInstance [id=%s, value=%s, type=%s]", id,
+                type == Type.OPAQUE ? ((byte[]) value).length + "Bytes" : value, type);
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mSingleResource.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mSingleResource.java
index d0cc98cb..b8b1b9ef 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mSingleResource.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/LwM2mSingleResource.java
@@ -135,7 +135,7 @@ public class LwM2mSingleResource implements LwM2mResource {
      * @exception NoSuchElementException use {@link #getValue()} instead.
      */
     @Override
-    public Map<Integer, ?> getValues() {
+    public Object getValue(int id) {
         throw new NoSuchElementException("There is no 'values' on single resources, use getValue() instead.");
     }
 
@@ -143,8 +143,16 @@ public class LwM2mSingleResource implements LwM2mResource {
      * @exception NoSuchElementException use {@link #getValue()} instead.
      */
     @Override
-    public Object getValue(int id) {
-        throw new NoSuchElementException("There is no 'values' on single resources, use getValue() instead.");
+    public LwM2mResourceInstance getInstance(int id) {
+        throw new NoSuchElementException("There is no 'instance' on single resources, use getValue() instead.");
+    }
+
+    /**
+     * @exception NoSuchElementException use {@link #getValue()} instead.
+     */
+    @Override
+    public Map<Integer, LwM2mResourceInstance> getInstances() {
+        throw new NoSuchElementException("There is no 'instances' on single resources, use getValue() instead.");
     }
 
     /**
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 7ee8ee81..d8b5ae3a 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
@@ -26,6 +26,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.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;
@@ -156,6 +157,8 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
             return LwM2mObjectInstance.class;
         } else if (path.isResource()) {
             return LwM2mResource.class;
+        } else if (path.isResourceInstance()) {
+            return LwM2mResourceInstance.class;
         }
         throw new IllegalArgumentException("invalid path level: " + path);
     }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java
index def3e6fd..6f094a35 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonDecoder.java
@@ -41,6 +41,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.node.codec.CodecException;
@@ -139,6 +140,33 @@ public class LwM2mNodeJsonDecoder {
                     throw new CodecException("One resource should be present in the payload [path:%s]", requestPath);
 
                 node = resourcesMap.values().iterator().next();
+            } else if (nodeClass == LwM2mResourceInstance.class) {
+                // validate we have resources for only 1 instance
+                if (jsonEntryByInstanceId.size() > 1)
+                    throw new CodecException("Only one instance expected in the payload [path:%s]", requestPath);
+
+                // Extract resources
+                Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(
+                        jsonEntryByInstanceId.values().iterator().next(), baseName, model, requestPath);
+
+                // validate there is only 1 resource
+                if (resourcesMap.size() != 1)
+                    throw new CodecException("One resource should be present in the payload [path:%s]", requestPath);
+
+                LwM2mResource resource = resourcesMap.values().iterator().next();
+                if (!resource.isMultiInstances()) {
+                    throw new CodecException("Resource should be multi Instances resource [path:%s]", requestPath);
+                }
+
+                if (resource.getInstances().isEmpty()) {
+                    throw new CodecException("Resource instances should not be not empty [path:%s]", requestPath);
+                }
+
+                if (resource.getInstances().size() > 1) {
+                    throw new CodecException("Resource instances should not be > 1 [path:%s]", requestPath);
+                }
+
+                node = resource.getInstance(requestPath.getResourceInstanceId());
             } else {
                 throw new IllegalArgumentException("invalid node class: " + nodeClass);
             }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonEncoder.java
index 63b6e02a..9575cac4 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/json/LwM2mNodeJsonEncoder.java
@@ -33,6 +33,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
@@ -174,6 +175,24 @@ public class LwM2mNodeJsonEncoder {
             resourceList = lwM2mResourceToJsonArrayEntry(null, timestamp, resource);
         }
 
+        @Override
+        public void visit(LwM2mResourceInstance instance) {
+            LOG.trace("Encoding resource instance {} into JSON", instance);
+            if (!requestPath.isResourceInstance()) {
+                throw new CodecException("Invalid request path %s for JSON resource instance encoding", requestPath);
+            }
+            baseName = requestPath.toString();
+
+            // get type for this resource
+            ResourceModel rSpec = model.getResourceModel(objectId, instance.getId());
+            Type expectedType = rSpec != null ? rSpec.type : instance.getType();
+
+            JsonArrayEntry jsonArrayEntry = createJsonArrayEntry(null, timestamp, instance.getType(), expectedType,
+                    instance.getValue());
+            resourceList = new ArrayList<>();
+            resourceList.add(jsonArrayEntry);
+        }
+
         private ArrayList<JsonArrayEntry> lwM2mResourceToJsonArrayEntry(String resourcePath, Long timestamp,
                 LwM2mResource resource) {
             // get type for this resource
@@ -183,46 +202,44 @@ public class LwM2mNodeJsonEncoder {
 
             // create JSON resource element
             if (resource.isMultiInstances()) {
-                for (Entry<Integer, ?> entry : resource.getValues().entrySet()) {
+
+                for (Entry<Integer, LwM2mResourceInstance> entry : resource.getInstances().entrySet()) {
                     // compute resource instance path
                     String resourceInstancePath;
+                    LwM2mResourceInstance instance = entry.getValue();
                     if (resourcePath == null || resourcePath.isEmpty()) {
                         resourceInstancePath = Integer.toString(entry.getKey());
                     } else {
                         resourceInstancePath = resourcePath + "/" + entry.getKey();
                     }
 
-                    // Create resource element
-                    JsonArrayEntry jsonResourceElt = new JsonArrayEntry();
-                    jsonResourceElt.setName(resourceInstancePath);
-                    jsonResourceElt.setTime(timestamp);
-
-                    // Convert value using expected type
-                    LwM2mPath lwM2mResourceInstancePath = new LwM2mPath(resourceInstancePath);
-                    Object convertedValue = converter.convertValue(entry.getValue(), resource.getType(), expectedType,
-                            lwM2mResourceInstancePath);
-                    this.setResourceValue(convertedValue, expectedType, jsonResourceElt, lwM2mResourceInstancePath);
-
-                    // Add it to the List
-                    resourcesList.add(jsonResourceElt);
+                    JsonArrayEntry jsonArrayEntry = createJsonArrayEntry(resourceInstancePath, timestamp,
+                            resource.getType(), expectedType, instance.getValue());
+                    resourcesList.add(jsonArrayEntry);
                 }
             } else {
-                // Create resource element
-                JsonArrayEntry jsonResourceElt = new JsonArrayEntry();
-                jsonResourceElt.setName(resourcePath);
-                jsonResourceElt.setTime(timestamp);
-
-                // Convert value using expected type
-                LwM2mPath lwM2mResourcePath = resourcePath != null ? new LwM2mPath(resourcePath) : null;
-                this.setResourceValue(converter.convertValue(resource.getValue(), resource.getType(), expectedType,
-                        lwM2mResourcePath), expectedType, jsonResourceElt, lwM2mResourcePath);
-
-                // Add it to the List
-                resourcesList.add(jsonResourceElt);
+                JsonArrayEntry jsonArrayEntry = createJsonArrayEntry(resourcePath, timestamp, resource.getType(),
+                        expectedType, resource.getValue());
+                resourcesList.add(jsonArrayEntry);
             }
             return resourcesList;
         }
 
+        private JsonArrayEntry createJsonArrayEntry(String name, Long timestamp, Type type, Type expectedType,
+                Object value) {
+            // Create resource element
+            JsonArrayEntry jsonResourceElt = new JsonArrayEntry();
+            jsonResourceElt.setName(name);
+            jsonResourceElt.setTime(timestamp);
+
+            // Convert value using expected type
+            LwM2mPath lwM2mResourcePath = name != null ? new LwM2mPath(name) : null;
+            Object convertedValue = converter.convertValue(value, type, expectedType, lwM2mResourcePath);
+            this.setResourceValue(convertedValue, expectedType, jsonResourceElt, lwM2mResourcePath);
+
+            return jsonResourceElt;
+        }
+
         private void setResourceValue(Object value, Type type, JsonArrayEntry jsonResource, LwM2mPath resourcePath) {
             LOG.trace("Encoding value {} in JSON", value);
 
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java
index 74b5d593..560ac2b6 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/opaque/LwM2mNodeOpaqueEncoder.java
@@ -24,6 +24,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
 import org.eclipse.leshan.core.util.Validate;
@@ -78,5 +79,10 @@ public class LwM2mNodeOpaqueEncoder {
             Object value = converter.convertValue(resource.getValue(), resource.getType(), Type.OPAQUE, path);
             encoded = (byte[]) value;
         }
+
+        @Override
+        public void visit(LwM2mResourceInstance instance) {
+            throw new UnsupportedOperationException("not yet implemented");
+        }
     }
 }
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 47c4c407..e3a9b0b2 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
@@ -26,6 +26,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
 import org.eclipse.leshan.core.util.Base64;
@@ -112,10 +113,15 @@ public class LwM2mNodeSenMLJsonEncoder {
             lwM2mResourceToSenMLRecord(null, resource);
         }
 
+        @Override
+        public void visit(LwM2mResourceInstance instance) {
+            throw new UnsupportedOperationException("not yet implemented");
+        }
+
         private void lwM2mResourceToSenMLRecord(String recordName, LwM2mResource resource) {
             // create resource element
             if (resource.isMultiInstances()) {
-                for (Entry<Integer, ?> entry : resource.getValues().entrySet()) {
+                for (Entry<Integer, LwM2mResourceInstance> entry : resource.getInstances().entrySet()) {
                     // compute record name for resource instance
                     String resourceInstanceRecordName;
                     if (recordName == null || recordName.isEmpty()) {
@@ -124,7 +130,7 @@ public class LwM2mNodeSenMLJsonEncoder {
                         resourceInstanceRecordName = recordName + "/" + entry.getKey();
                     }
 
-                    addSenMLRecord(resourceInstanceRecordName, resource, entry.getValue());
+                    addSenMLRecord(resourceInstanceRecordName, resource, entry.getValue().getValue());
                 }
             } else {
                 addSenMLRecord(recordName, resource, resource.getValue());
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 d64227a2..4b8af53f 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
@@ -24,6 +24,7 @@ import org.eclipse.leshan.core.model.ResourceModel;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mPath;
+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;
@@ -35,20 +36,28 @@ public class LwM2mNodeTextDecoder {
     private static final Logger LOG = LoggerFactory.getLogger(LwM2mNodeTextDecoder.class);
 
     public static LwM2mNode decode(byte[] content, LwM2mPath path, LwM2mModel model) throws CodecException {
-        if (!path.isResource())
-            throw new CodecException("Invalid path %s : TextDecoder decodes resource only", path);
+        if (!path.isResource() && !path.isResourceInstance())
+            throw new CodecException("Invalid path %s : TextDecoder decodes resource OR resource instance only", path);
 
         ResourceModel rDesc = model.getResourceModel(path.getObjectId(), path.getResourceId());
 
         String strValue = content != null ? new String(content, StandardCharsets.UTF_8) : "";
-        if (rDesc != null) {
-            return LwM2mSingleResource.newResource(path.getResourceId(), parseTextValue(strValue, rDesc.type, path),
-                    rDesc.type);
-        } else {
+
+        if (path.isResource()) {
+            if (rDesc != null) {
+                return LwM2mSingleResource.newResource(path.getResourceId(), parseTextValue(strValue, rDesc.type, path),
+                        rDesc.type);
+            }
             // unknown resource, returning a default string value
             return LwM2mSingleResource.newStringResource(path.getResourceId(), strValue);
         }
 
+        if (rDesc != null) {
+            return LwM2mResourceInstance.newInstance(path.getResourceInstanceId(),
+                    parseTextValue(strValue, rDesc.type, path), rDesc.type);
+        }
+        // unknown resource, returning a default string value
+        return LwM2mResourceInstance.newStringInstance(path.getResourceInstanceId(), strValue);
     }
 
     private static Object parseTextValue(String value, Type type, LwM2mPath path) throws CodecException {
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 99cf2cf9..f8ba3de1 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
@@ -27,6 +27,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
@@ -88,6 +89,30 @@ public class LwM2mNodeTextEncoder {
                         "Unable to encode value for resource {} without type(probably a executable one)", path);
             }
 
+            String strValue = getStringValue(expectedType, val);
+
+            encoded = strValue.getBytes(StandardCharsets.UTF_8);
+        }
+
+        @Override
+        public void visit(LwM2mResourceInstance instance) {
+            LOG.trace("Encoding resource instance {} into text", instance);
+
+            ResourceModel rSpec = model.getResourceModel(path.getObjectId(), instance.getId());
+            Type expectedType = rSpec != null ? rSpec.type : instance.getType();
+            Object val = converter.convertValue(instance.getValue(), instance.getType(), expectedType, path);
+
+            if (expectedType == null) {
+                throw new CodecException(
+                        "Unable to encode value for resource {} without type(probably a executable one)", path);
+            }
+
+            String strValue = getStringValue(expectedType, val);
+
+            encoded = strValue.getBytes(StandardCharsets.UTF_8);
+        }
+
+        private String getStringValue(Type expectedType, Object val) {
             String strValue;
             switch (expectedType) {
             case INTEGER:
@@ -113,8 +138,7 @@ public class LwM2mNodeTextEncoder {
             default:
                 throw new CodecException("Cannot encode %s in text format for %s", val, path);
             }
-
-            encoded = strValue.getBytes(StandardCharsets.UTF_8);
+            return strValue;
         }
     }
 }
\ No newline at end of file
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java
index 6bfe78a1..74f22a35 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvDecoder.java
@@ -31,6 +31,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.tlv.Tlv;
@@ -159,13 +160,27 @@ public class LwM2mNodeTlvDecoder {
                 return (T) parseResourceTlv(tlv, path, model);
             } else {
                 Type expectedRscType = getResourceType(path, model);
-                return (T) LwM2mMultipleResource.newResource(path.getResourceId(),
-                        parseTlvValues(tlvs, expectedRscType, path), expectedRscType);
+                return (T) parseResourceInstancesTlv(tlvs, path, expectedRscType);
             }
-        } else {
-            throw new IllegalArgumentException("invalid node class: " + nodeClass);
         }
 
+        // Resource Instance
+        else if (nodeClass == LwM2mResourceInstance.class) {
+            if (tlvs.length == 0) {
+                throw new CodecException("TLV payload is mandatory for resource instance %s", path);
+            } else if (tlvs.length > 1) {
+                throw new CodecException("TLV payload must contain only 1 resource instance for %s", path);
+            } else {
+                Tlv tlv = tlvs[0];
+                if (path.isResourceInstance() && path.getResourceInstanceId() != tlv.getIdentifier()) {
+                    throw new CodecException("Id conflict between path [%s] and resource instance TLV [resource id=%s]",
+                            path, tlv.getIdentifier());
+                }
+                Type expectedType = getResourceType(path, model);
+                return (T) parseResourceInstanceTlv(tlv, path, expectedType);
+            }
+        }
+        throw new IllegalArgumentException("invalid node class: " + nodeClass);
     }
 
     private static LwM2mObjectInstance parseObjectInstanceTlv(Tlv[] rscTlvs, int objectId, int instanceId,
@@ -205,8 +220,7 @@ public class LwM2mNodeTlvDecoder {
         Integer resourceId = tlv.getIdentifier();
         switch (tlv.getType()) {
         case MULTIPLE_RESOURCE:
-            return LwM2mMultipleResource.newResource(resourceId,
-                    parseTlvValues(tlv.getChildren(), expectedType, resourcePath), expectedType);
+            return parseResourceInstancesTlv(tlv.getChildren(), resourcePath, expectedType);
         case RESOURCE_VALUE:
             return LwM2mSingleResource.newResource(resourceId,
                     parseTlvValue(tlv.getValue(), expectedType, resourcePath), expectedType);
@@ -215,22 +229,32 @@ public class LwM2mNodeTlvDecoder {
         }
     }
 
-    private static Map<Integer, Object> parseTlvValues(Tlv[] tlvs, Type expectedType, LwM2mPath path)
-            throws CodecException {
-        Map<Integer, Object> values = new HashMap<>();
-        for (Tlv tlvChild : tlvs) {
-            if (tlvChild.getType() != TlvType.RESOURCE_INSTANCE)
-                throw new CodecException("Expected TLV of type RESOURCE_INSTANCE but was %s for path %s",
-                        tlvChild.getType().name(), path);
-
-            Object resourceInstance = parseTlvValue(tlvChild.getValue(), expectedType, path);
-            Object previousResourceInstance = values.put(tlvChild.getIdentifier(), resourceInstance);
+    private static LwM2mMultipleResource parseResourceInstancesTlv(Tlv[] tlvs, LwM2mPath resourcePath,
+            Type expectedType) throws CodecException {
+        Map<Integer, LwM2mResourceInstance> instances = new HashMap<>(tlvs.length);
+        for (Tlv tlv : tlvs) {
+            LwM2mResourceInstance resourceInstance = parseResourceInstanceTlv(tlv,
+                    resourcePath.append(tlv.getIdentifier()), expectedType);
+            LwM2mResourceInstance previousResourceInstance = instances.put(tlv.getIdentifier(), resourceInstance);
             if (previousResourceInstance != null) {
                 throw new CodecException("2 RESOURCE_INSTANCE nodes (%s,%s) with the same identifier %d for path %s",
-                        previousResourceInstance, resourceInstance, tlvChild.getIdentifier(), path);
+                        previousResourceInstance, resourceInstance, tlv.getIdentifier(), resourcePath);
             }
         }
-        return values;
+        return new LwM2mMultipleResource(resourcePath.getResourceId(), expectedType, instances.values());
+    }
+
+    private static LwM2mResourceInstance parseResourceInstanceTlv(Tlv tlv, LwM2mPath resourceInstancePath,
+            Type expectedType) throws CodecException {
+        Integer resourceInstanceId = tlv.getIdentifier();
+        if (tlv.getType() != TlvType.RESOURCE_INSTANCE) {
+            throw new CodecException("Expected TLV of type RESOURCE_INSTANCE but was %s for path %s", tlv.getType(),
+                    resourceInstancePath);
+        } else {
+            return LwM2mResourceInstance.newInstance(resourceInstanceId,
+                    parseTlvValue(tlv.getValue(), expectedType, resourceInstancePath), expectedType);
+        }
+
     }
 
     private static Object parseTlvValue(byte[] value, Type expectedType, LwM2mPath path) throws CodecException {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java
index b697f5d0..fa9f0f7b 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/tlv/LwM2mNodeTlvEncoder.java
@@ -31,6 +31,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.node.codec.CodecException;
 import org.eclipse.leshan.core.node.codec.LwM2mValueConverter;
@@ -148,14 +149,11 @@ public class LwM2mNodeTlvEncoder {
 
             Tlv rTlv;
             if (resource.isMultiInstances()) {
-                Tlv[] instances = new Tlv[resource.getValues().size()];
+                Tlv[] instances = new Tlv[resource.getInstances().size()];
                 int i = 0;
-                for (Entry<Integer, ?> entry : resource.getValues().entrySet()) {
-                    LwM2mPath resourceInstancePath = resourcePath.append(entry.getKey());
-                    Object convertedValue = converter.convertValue(entry.getValue(), resource.getType(), expectedType,
-                            resourceInstancePath);
-                    instances[i] = new Tlv(TlvType.RESOURCE_INSTANCE, null,
-                            this.encodeTlvValue(convertedValue, expectedType, resourceInstancePath), entry.getKey());
+                for (LwM2mResourceInstance resourceInstance : resource.getInstances().values()) {
+                    LwM2mPath resourceInstancePath = resourcePath.append(resourceInstance.getId());
+                    instances[i] = encodeResourceInstance(resourceInstance, resourceInstancePath, expectedType);
                     i++;
                 }
                 rTlv = new Tlv(TlvType.MULTIPLE_RESOURCE, instances, null, resource.getId());
@@ -168,6 +166,30 @@ public class LwM2mNodeTlvEncoder {
             return rTlv;
         }
 
+        @Override
+        public void visit(LwM2mResourceInstance resourceInstance) {
+            LOG.trace("Encoding resource instance {} into TLV", resourceInstance);
+
+            ResourceModel rSpec = model.getResourceModel(path.getObjectId(), path.getResourceId());
+            Type expectedType = rSpec != null ? rSpec.type : resourceInstance.getType();
+
+            Tlv rTlv = encodeResourceInstance(resourceInstance, path, expectedType);
+
+            try {
+                out.write(TlvEncoder.encode(new Tlv[] { rTlv }).array());
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        private Tlv encodeResourceInstance(LwM2mResourceInstance resourceInstance, LwM2mPath resourceInstancePath,
+                Type expectedType) {
+            Object convertedValue = converter.convertValue(resourceInstance.getValue(), resourceInstance.getType(),
+                    expectedType, resourceInstancePath);
+            return new Tlv(TlvType.RESOURCE_INSTANCE, null,
+                    this.encodeTlvValue(convertedValue, expectedType, resourceInstancePath), resourceInstance.getId());
+        }
+
         private byte[] encodeTlvValue(Object value, Type type, LwM2mPath path) {
             LOG.trace("Encoding value {} in TLV", value);
             if (type == null || type == Type.NONE) {
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/AbstractDownlinkRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/AbstractDownlinkRequest.java
index 0645b089..e30e7376 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/AbstractDownlinkRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/AbstractDownlinkRequest.java
@@ -32,9 +32,6 @@ public abstract class AbstractDownlinkRequest<T extends LwM2mResponse> implement
         if (path == null)
             throw new InvalidRequestException("path is mandatory");
 
-        if (path.isResourceInstance())
-            throw new InvalidRequestException("downlink request cannot target resource instance path: %s ", path);
-
         this.path = path;
     }
 
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapWriteRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapWriteRequest.java
index de068d14..fa6a4a0e 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapWriteRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/BootstrapWriteRequest.java
@@ -38,6 +38,10 @@ public class BootstrapWriteRequest extends AbstractDownlinkRequest<BootstrapWrit
         if (target.isRoot())
             throw new InvalidRequestException("BootstrapWrite request cannot target root path");
 
+        if (target.isResourceInstance())
+            throw new InvalidRequestException("BootstrapWrite request cannot target resource instance path: %s ",
+                    target);
+
         if (node == null)
             throw new InvalidRequestException("new node value is mandatory for %s", target);
 
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java
index 062c0c02..07fe657f 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CancelObservationRequest.java
@@ -38,6 +38,9 @@ public class CancelObservationRequest extends AbstractDownlinkRequest<CancelObse
         super(observation.getPath());
         if (getPath().isRoot())
             throw new InvalidRequestException("Observe request cannot target root path");
+        if (getPath().isResourceInstance())
+            throw new InvalidRequestException(
+                    "Observe request cannot target resource instance path: %s (not yet implemented)", getPath());
         this.observation = observation;
     }
 
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/DiscoverRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/DiscoverRequest.java
index bab7b353..b157a5ee 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/DiscoverRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/DiscoverRequest.java
@@ -69,6 +69,8 @@ public class DiscoverRequest extends AbstractDownlinkRequest<DiscoverResponse> {
         super(target);
         if (target.isRoot())
             throw new InvalidRequestException("Discover request cannot target root path");
+        if (target.isResourceInstance())
+            throw new InvalidRequestException("Discover request cannot target resource instance path: %s ", target);
     }
 
     @Override
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ObserveRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ObserveRequest.java
index dd8dabba..9939b205 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ObserveRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ObserveRequest.java
@@ -137,6 +137,9 @@ public class ObserveRequest extends AbstractDownlinkRequest<ObserveResponse> {
         super(target);
         if (target.isRoot())
             throw new InvalidRequestException("Observe request cannot target root path");
+        if (target.isResourceInstance())
+            throw new InvalidRequestException(
+                    "Observe request cannot target resource instance path: %s (not yet implemented)", target);
 
         this.format = format;
         if (context == null || context.isEmpty())
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ReadRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ReadRequest.java
index 47bb8718..822df662 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/ReadRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/ReadRequest.java
@@ -80,6 +80,18 @@ public class ReadRequest extends AbstractDownlinkRequest<ReadResponse> {
         this(null, new LwM2mPath(objectId, objectInstanceId, resourceId));
     }
 
+    /**
+     * Creates a request for reading a specific resource from a client.
+     * 
+     * @param objectId the object ID of the resource
+     * @param objectInstanceId the object instance ID
+     * @param resourceId the (individual) resource's ID
+     * @param resourceInstanceId the resource instance's ID
+     */
+    public ReadRequest(int objectId, int objectInstanceId, int resourceId, int resourceInstanceId) {
+        this(null, new LwM2mPath(objectId, objectInstanceId, resourceId, resourceInstanceId));
+    }
+
     /**
      * Creates a request for reading a specific resource from a client.
      * 
@@ -92,6 +104,20 @@ public class ReadRequest extends AbstractDownlinkRequest<ReadResponse> {
         this(format, new LwM2mPath(objectId, objectInstanceId, resourceId));
     }
 
+    /**
+     * Creates a request for reading a specific resource from a client.
+     * 
+     * @param format the desired format for the response (TLV, JSON, TEXT or OPAQUE)
+     * @param objectId the object ID of the resource
+     * @param objectInstanceId the object instance ID
+     * @param resourceId the (individual) resource's ID
+     * @param resourceInstanceId the resource instance's ID
+     */
+    public ReadRequest(ContentFormat format, int objectId, int objectInstanceId, int resourceId,
+            int resourceInstanceId) {
+        this(format, new LwM2mPath(objectId, objectInstanceId, resourceId, resourceInstanceId));
+    }
+
     /**
      * Create a request for reading an object/instance/resource targeted by a specific path.
      * 
@@ -117,7 +143,6 @@ public class ReadRequest extends AbstractDownlinkRequest<ReadResponse> {
         super(target);
         if (target.isRoot())
             throw new InvalidRequestException("Read request cannot target root path");
-
         this.format = format;
     }
 
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteAttributesRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteAttributesRequest.java
index a6beac5c..b8979d05 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteAttributesRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteAttributesRequest.java
@@ -46,6 +46,9 @@ public class WriteAttributesRequest extends AbstractDownlinkRequest<WriteAttribu
         super(path);
         if (path.isRoot())
             throw new InvalidRequestException("WriteAttributes request cannot target root path");
+        if (path.isResourceInstance())
+            throw new InvalidRequestException(
+                    "WriteAttributes request cannot target resource instance path: %s (not yet implemented)", path);
 
         if (attributes == null)
             throw new InvalidRequestException("attributes are mandatory for %s", path);
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java
index b8c1aa3c..25b7de6d 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/WriteRequest.java
@@ -25,6 +25,7 @@ import org.eclipse.leshan.core.node.LwM2mNode;
 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.request.exception.InvalidRequestException;
@@ -278,6 +279,41 @@ public class WriteRequest extends AbstractDownlinkRequest<WriteResponse> {
                 LwM2mMultipleResource.newResource(resourceId, values, type));
     }
 
+    /**
+     * Request to write a specific resource instance from a client.
+     *
+     * @param mode the mode of the request : replace or update.
+     * @param contentFormat Format of the payload (TLV or JSON).
+     * @param objectId the object ID of the resource
+     * @param objectInstanceId the object instance ID
+     * @param resourceId the (individual) resource's ID
+     * @param resourceInstanceId the resource instance's ID
+     * @param value the resource instance (id-&gt;value) to write.
+     * @param type the data type of the resource.
+     */
+    public WriteRequest(Mode mode, ContentFormat contentFormat, int objectId, int objectInstanceId, int resourceId,
+            int resourceInstanceId, Object value, Type type) {
+        this(mode, contentFormat, new LwM2mPath(objectId, objectInstanceId, resourceId, resourceInstanceId),
+                LwM2mResourceInstance.newInstance(resourceInstanceId, value, type));
+    }
+
+    /**
+     * Request to write a specific resource instance from a client, MODE = REPLACE.
+     *
+     * @param contentFormat Format of the payload (TLV or JSON).
+     * @param objectId the object ID of the resource
+     * @param objectInstanceId the object instance ID
+     * @param resourceId the (individual) resource's ID
+     * @param resourceInstanceId the resource instance's ID
+     * @param value the resource instance (id-&gt;value) to write.
+     * @param type the data type of the resource.
+     */
+    public WriteRequest(ContentFormat contentFormat, int objectId, int objectInstanceId, int resourceId,
+            int resourceInstanceId, Object value, Type type) {
+        this(Mode.REPLACE, contentFormat, new LwM2mPath(objectId, objectInstanceId, resourceId, resourceInstanceId),
+                LwM2mResourceInstance.newInstance(resourceInstanceId, value, type));
+    }
+
     // ***************** generic constructor ****************** //
     /**
      * A generic constructor to write request.
@@ -323,19 +359,30 @@ public class WriteRequest extends AbstractDownlinkRequest<WriteResponse> {
 
         // Validate content format
         if (ContentFormat.TEXT == format || ContentFormat.OPAQUE == format) {
-            if (!getPath().isResource()) {
+            if (!getPath().isResource() && !getPath().isResourceInstance()) {
                 throw new InvalidRequestException(
                         "Invalid format for %s: %s format must be used only for single resources", target, format);
             } else {
-                LwM2mResource resource = (LwM2mResource) node;
-                if (resource.isMultiInstances()) {
-                    throw new InvalidRequestException(
-                            "Invalid format for path %s: format must be used only for single resources", target,
-                            format);
-                } else if (resource.getType() != Type.OPAQUE && format == ContentFormat.OPAQUE) {
-                    throw new InvalidRequestException(
-                            "Invalid format for %s: OPAQUE format must be used only for byte array single resources",
-                            target);
+                if (node instanceof LwM2mResource) {
+                    LwM2mResource resource = (LwM2mResource) node;
+                    if (resource.isMultiInstances()) {
+                        throw new InvalidRequestException(
+                                "Invalid format for path %s: format must be used only for single resources", target,
+                                format);
+                    } else if (resource.getType() != Type.OPAQUE && format == ContentFormat.OPAQUE) {
+                        throw new InvalidRequestException(
+                                "Invalid format for %s: OPAQUE format must be used only for byte array single resources",
+                                target);
+                    }
+                }
+
+                if (node instanceof LwM2mResourceInstance) {
+                    LwM2mResourceInstance resourceInstance = (LwM2mResourceInstance) node;
+                    if (resourceInstance.getType() != Type.OPAQUE && format == ContentFormat.OPAQUE) {
+                        throw new InvalidRequestException(
+                                "Invalid format for %s: OPAQUE format must be used only for byte array single resources",
+                                target);
+                    }
                 }
             }
         }
diff --git a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
index bd13550c..3ca7abc8 100644
--- a/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
+++ b/leshan-server-cf/src/main/java/org/eclipse/leshan/server/californium/request/CoapRequestBuilder.java
@@ -258,6 +258,11 @@ public class CoapRequestBuilder implements DownlinkRequestVisitor {
         if (path.getResourceId() != null) {
             coapRequest.getOptions().addUriPath(Integer.toString(path.getResourceId()));
         }
+
+        // resourceInstanceId
+        if (path.getResourceInstanceId() != null) {
+            coapRequest.getOptions().addUriPath(Integer.toString(path.getResourceInstanceId()));
+        }
     }
 
     protected void applyLowerLayerConfig(Request coapRequest) {
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/json/LwM2mNodeSerializer.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/json/LwM2mNodeSerializer.java
index 12092261..d9c632c2 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/json/LwM2mNodeSerializer.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/servlet/json/LwM2mNodeSerializer.java
@@ -22,6 +22,7 @@ import org.eclipse.leshan.core.node.LwM2mNode;
 import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.util.Hex;
 
 import com.google.gson.JsonElement;
@@ -45,12 +46,12 @@ public class LwM2mNodeSerializer implements JsonSerializer<LwM2mNode> {
             LwM2mResource rsc = (LwM2mResource) src;
             if (rsc.isMultiInstances()) {
                 JsonObject values = new JsonObject();
-                for (Entry<Integer, ?> entry : rsc.getValues().entrySet()) {
+                for (Entry<Integer, LwM2mResourceInstance> entry : rsc.getInstances().entrySet()) {
                     if (rsc.getType() == org.eclipse.leshan.core.model.ResourceModel.Type.OPAQUE) {
                         values.addProperty(entry.getKey().toString(),
-                                new String(Hex.encodeHex((byte[]) entry.getValue())));
+                                new String(Hex.encodeHex((byte[]) entry.getValue().getValue())));
                     } else {
-                        values.add(entry.getKey().toString(), context.serialize(entry.getValue()));
+                        values.add(entry.getKey().toString(), context.serialize(entry.getValue().getValue()));
                     }
                 }
                 element.add("values", values);


Test patch:
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/node/codec/LwM2mNodeDecoderTest.java
index c1eb4c60..c1d27b8a 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
@@ -41,8 +41,8 @@ import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.tlv.Tlv;
-import org.eclipse.leshan.core.tlv.TlvEncoder;
 import org.eclipse.leshan.core.tlv.Tlv.TlvType;
+import org.eclipse.leshan.core.tlv.TlvEncoder;
 import org.eclipse.leshan.core.util.Hex;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -121,7 +121,7 @@ public class LwM2mNodeDecoderTest {
         assertEquals("1.0", oInstance.getResource(3).getValue());
         assertNull(oInstance.getResource(4));
         assertNull(oInstance.getResource(5));
-        assertEquals(2, oInstance.getResource(6).getValues().size());
+        assertEquals(2, oInstance.getResource(6).getInstances().size());
         assertEquals(1L, oInstance.getResource(6).getValue(0));
         assertEquals(5L, oInstance.getResource(6).getValue(1));
         assertEquals(3800L, oInstance.getResource(7).getValue(0));
@@ -130,7 +130,7 @@ public class LwM2mNodeDecoderTest {
         assertEquals(900L, oInstance.getResource(8).getValue(1));
         assertEquals(100L, oInstance.getResource(9).getValue());
         assertEquals(15L, oInstance.getResource(10).getValue());
-        assertEquals(1, oInstance.getResource(11).getValues().size());
+        assertEquals(1, oInstance.getResource(11).getInstances().size());
         assertEquals(0L, oInstance.getResource(11).getValue(0));
         assertNull(oInstance.getResource(12));
         assertEquals(new Date(1367491215000L), oInstance.getResource(13).getValue());
@@ -147,7 +147,7 @@ public class LwM2mNodeDecoderTest {
         LwM2mObjectInstance oInstance0 = oObject.getInstance(0);
         assertEquals(1L, oInstance0.getResource(0).getValue());
         assertEquals(0L, oInstance0.getResource(1).getValue());
-        assertEquals(1, oInstance0.getResource(2).getValues().size());
+        assertEquals(1, oInstance0.getResource(2).getInstances().size());
         assertEquals(7L, oInstance0.getResource(2).getValue(127));
         assertEquals(127L, oInstance0.getResource(3).getValue());
 
@@ -155,7 +155,7 @@ public class LwM2mNodeDecoderTest {
         LwM2mObjectInstance oInstance2 = oObject.getInstance(2);
         assertEquals(3L, oInstance2.getResource(0).getValue());
         assertEquals(0L, oInstance2.getResource(1).getValue());
-        assertEquals(2, oInstance2.getResource(2).getValues().size());
+        assertEquals(2, oInstance2.getResource(2).getInstances().size());
         assertEquals(7L, oInstance2.getResource(2).getValue(127));
         assertEquals(1L, oInstance2.getResource(2).getValue(310));
         assertEquals(127L, oInstance2.getResource(3).getValue());
@@ -176,7 +176,7 @@ public class LwM2mNodeDecoderTest {
         assertEquals(0, instance.getId());
 
         // instance 1
-        assertEquals(2, instance.getResource(0).getValues().size());
+        assertEquals(2, instance.getResource(0).getInstances().size());
         assertEquals(new ObjectLink(66, 0), instance.getResource(0).getValue(0));
         assertEquals(new ObjectLink(66, 1), instance.getResource(0).getValue(1));
         assertEquals("8613800755500", instance.getResource(1).getValue());
@@ -338,7 +338,7 @@ public class LwM2mNodeDecoderTest {
                 model);
 
         assertEquals(6, resource.getId());
-        assertEquals(2, resource.getValues().size());
+        assertEquals(2, resource.getInstances().size());
         assertEquals(1L, resource.getValue(0));
         assertEquals(5L, resource.getValue(1));
     }
@@ -353,7 +353,7 @@ public class LwM2mNodeDecoderTest {
                 model);
 
         assertEquals(6, resource.getId());
-        assertEquals(2, resource.getValues().size());
+        assertEquals(2, resource.getInstances().size());
         assertEquals(1L, resource.getValue(0));
         assertEquals(5L, resource.getValue(1));
     }
@@ -447,7 +447,7 @@ public class LwM2mNodeDecoderTest {
         assertNotNull(resource);
         assertTrue(resource instanceof LwM2mMultipleResource);
         assertEquals(6, resource.getId());
-        assertTrue(resource.getValues().isEmpty());
+        assertTrue(resource.getInstances().isEmpty());
     }
 
     @Test(expected = CodecException.class)
@@ -743,7 +743,7 @@ public class LwM2mNodeDecoderTest {
         assertNotNull(resource);
         assertTrue(resource instanceof LwM2mMultipleResource);
         assertEquals(6, resource.getId());
-        assertTrue(resource.getValues().isEmpty());
+        assertTrue(resource.getInstances().isEmpty());
 
         // with empty resources list and base name
         b = new StringBuilder();
@@ -753,7 +753,7 @@ public class LwM2mNodeDecoderTest {
         assertNotNull(resource);
         assertTrue(resource instanceof LwM2mMultipleResource);
         assertEquals(6, resource.getId());
-        assertTrue(resource.getValues().isEmpty());
+        assertTrue(resource.getInstances().isEmpty());
     }
 
     @Test
@@ -806,6 +806,6 @@ public class LwM2mNodeDecoderTest {
         assertNotNull(resource);
         assertTrue(resource instanceof LwM2mMultipleResource);
         assertEquals(11, resource.getId());
-        assertTrue(resource.getValues().size() == 2);
+        assertTrue(resource.getInstances().size() == 2);
     }
 }
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 150c1957..9cbf4e93 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
@@ -32,6 +32,7 @@ import org.eclipse.leshan.core.node.LwM2mObject;
 import org.eclipse.leshan.core.node.LwM2mObjectInstance;
 import org.eclipse.leshan.core.node.LwM2mPath;
 import org.eclipse.leshan.core.node.LwM2mResource;
+import org.eclipse.leshan.core.node.LwM2mResourceInstance;
 import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.request.ContentFormat;
@@ -221,6 +222,25 @@ public class LwM2mNodeEncoderTest {
         Assert.assertEquals(expected, new String(encoded));
     }
 
+    @Test
+    public void json_timestamped_resource_instances() throws CodecException {
+        List<TimestampedLwM2mNode> data = new ArrayList<>();
+        data.add(new TimestampedLwM2mNode(500L, LwM2mResourceInstance.newFloatInstance(0, 22.9)));
+        data.add(new TimestampedLwM2mNode(510L, LwM2mResourceInstance.newFloatInstance(0, 22.4)));
+        data.add(new TimestampedLwM2mNode(520L, LwM2mResourceInstance.newFloatInstance(0, 24.1)));
+
+        byte[] encoded = encoder.encodeTimestampedData(data, ContentFormat.JSON, new LwM2mPath(1024, 0, 1, 0), model);
+
+        StringBuilder b = new StringBuilder();
+        b.append("{\"bn\":\"/1024/0/1/0\",\"e\":[");
+        b.append("{\"v\":22.9,\"t\":500},");
+        b.append("{\"v\":22.4,\"t\":510},");
+        b.append("{\"v\":24.1,\"t\":520}]}");
+
+        String expected = b.toString();
+        Assert.assertEquals(expected, new String(encoded));
+    }
+
     @Test
     public void json_encode_timestamped_instances() throws CodecException {
         List<TimestampedLwM2mNode> data = new ArrayList<>();
diff --git a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java
index adab7821..004b1e3b 100644
--- a/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java
+++ b/leshan-integration-tests/src/test/java/org/eclipse/leshan/integration/tests/BootstrapTest.java
@@ -264,7 +264,7 @@ public class BootstrapTest {
         LwM2mObjectInstance instance = acl.getInstance(0);
         assertEquals(3l, instance.getResource(0).getValue());
         assertEquals(0l, instance.getResource(1).getValue());
-        assertEquals(1l, instance.getResource(2).getValues().get(3333));
+        assertEquals(1l, instance.getResource(2).getValue(3333));
         assertEquals(2222l, instance.getResource(3).getValue());
         instance = acl.getInstance(1);
         assertEquals(4l, instance.getResource(0).getValue());
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 f05334b6..caa2bec2 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
@@ -13,7 +13,6 @@
  * Contributors:
  *     Zebra Technologies - initial API and implementation
  *******************************************************************************/
-
 package org.eclipse.leshan.integration.tests;
 
 import static org.junit.Assert.*;
@@ -75,6 +74,9 @@ public class IntegrationTestHelper {
     public static final int OBJLNK_SINGLE_INSTANCE_RESOURCE_ID = 7;
     public static final int INTEGER_MANDATORY_RESOURCE_ID = 8;
     public static final int STRING_MANDATORY_RESOURCE_ID = 9;
+    public static final int STRING_RESOURCE_INSTANCE_ID = 10;
+
+    public static final String MULTI_INSTANCE = "multiinstance";
 
     LeshanServer server;
     LeshanClient client;
@@ -112,9 +114,11 @@ public class IntegrationTestHelper {
                 Operations.RW, false, true, Type.INTEGER, null, null, null);
         ResourceModel stringmandatoryfield = new ResourceModel(STRING_MANDATORY_RESOURCE_ID, "stringmandatory",
                 Operations.RW, false, true, Type.STRING, null, null, null);
+        ResourceModel multiInstance = new ResourceModel(STRING_RESOURCE_INSTANCE_ID, MULTI_INSTANCE, Operations.RW,
+                true, false, Type.STRING, null, null, null);
         objectModels.add(new ObjectModel(TEST_OBJECT_ID, "testobject", null, ObjectModel.DEFAULT_VERSION, true, false,
                 stringfield, booleanfield, integerfield, floatfield, timefield, opaquefield, objlnkfield,
-                objlnkSinglefield, integermandatoryfield, stringmandatoryfield));
+                objlnkSinglefield, integermandatoryfield, stringmandatoryfield, multiInstance));
 
         return objectModels;
     }
@@ -168,6 +172,7 @@ public class IntegrationTestHelper {
         // Build Client
         LeshanClientBuilder builder = new LeshanClientBuilder(currentEndpointIdentifier.get());
         builder.setDecoder(new DefaultLwM2mNodeDecoder(true));
+        builder.setEncoder(new DefaultLwM2mNodeEncoder(true));
         builder.setAdditionalAttributes(additionalAttributes);
         builder.setObjects(objects);
         client = builder.build();
@@ -182,6 +187,7 @@ public class IntegrationTestHelper {
 
     protected LeshanServerBuilder createServerBuilder() {
         LeshanServerBuilder builder = new LeshanServerBuilder();
+        builder.setDecoder(new DefaultLwM2mNodeDecoder(true));
         builder.setEncoder(new DefaultLwM2mNodeEncoder(true));
         builder.setObjectModelProvider(new StaticModelProvider(createObjectModels()));
         builder.setLocalAddress(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0));
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
index 5b6e2d39..bdeed11d 100644
--- 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
@@ -15,7 +15,6 @@
  *     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.*;
@@ -30,6 +29,7 @@ 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;
@@ -119,6 +119,67 @@ public class ReadTest {
         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
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
index 9158d6c9..4a4a20d0 100644
--- 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
@@ -15,9 +15,9 @@
  *     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;
@@ -34,6 +34,7 @@ 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;
@@ -107,7 +108,7 @@ public class WriteTest {
 
         // read resource to check the value changed
         ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(TEST_OBJECT_ID, 0, STRING_RESOURCE_ID));
+                new ReadRequest(format, TEST_OBJECT_ID, 0, STRING_RESOURCE_ID));
         LwM2mResource resource = (LwM2mResource) readResponse.getContent();
         assertEquals(expectedvalue, resource.getValue());
     }
@@ -150,7 +151,7 @@ public class WriteTest {
 
         // read resource to check the value changed
         ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(TEST_OBJECT_ID, 0, BOOLEAN_RESOURCE_ID));
+                new ReadRequest(format, TEST_OBJECT_ID, 0, BOOLEAN_RESOURCE_ID));
         LwM2mResource resource = (LwM2mResource) readResponse.getContent();
         assertEquals(expectedvalue, resource.getValue());
     }
@@ -193,11 +194,57 @@ public class WriteTest {
 
         // read resource to check the value changed
         ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(TEST_OBJECT_ID, 0, INTEGER_RESOURCE_ID));
+                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);
+    }
+
+    @Test
+    public void can_write_string_resource_old_json_instance() throws InterruptedException {
+        write_string_resource_instance(ContentFormat.fromCode(ContentFormat.OLD_JSON_CODE));
+    }
+
+    @Test
+    public void can_write_string_resource_text_instance() throws InterruptedException {
+        write_string_resource_instance(ContentFormat.TEXT);
+    }
+
+    @Test
+    public void can_write_string_resource_tlv_instance() throws InterruptedException {
+        write_string_resource_instance(ContentFormat.TLV);
+    }
+
+    @Test
+    public void can_write_string_resource_old_tlv_instance() throws InterruptedException {
+        write_string_resource_instance(ContentFormat.fromCode(ContentFormat.OLD_TLV_CODE));
+    }
+
+    private void write_string_resource_instance(ContentFormat format) 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, 0, 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, 0));
+
+        // 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);
@@ -236,7 +283,7 @@ public class WriteTest {
 
         // read resource to check the value changed
         ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(TEST_OBJECT_ID, 0, FLOAT_RESOURCE_ID));
+                new ReadRequest(format, TEST_OBJECT_ID, 0, FLOAT_RESOURCE_ID));
         LwM2mResource resource = (LwM2mResource) readResponse.getContent();
         assertEquals(expectedvalue, resource.getValue());
     }
@@ -279,7 +326,7 @@ public class WriteTest {
 
         // read resource to check the value changed
         ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(TEST_OBJECT_ID, 0, TIME_RESOURCE_ID));
+                new ReadRequest(format, TEST_OBJECT_ID, 0, TIME_RESOURCE_ID));
         LwM2mResource resource = (LwM2mResource) readResponse.getContent();
         assertEquals(expectedvalue, resource.getValue());
     }
@@ -327,7 +374,7 @@ public class WriteTest {
 
         // read resource to check the value changed
         ReadResponse readResponse = helper.server.send(helper.getCurrentRegistration(),
-                new ReadRequest(TEST_OBJECT_ID, 0, OPAQUE_RESOURCE_ID));
+                new ReadRequest(format, TEST_OBJECT_ID, 0, OPAQUE_RESOURCE_ID));
         LwM2mResource resource = (LwM2mResource) readResponse.getContent();
         assertArrayEquals(expectedvalue, (byte[]) resource.getValue());
     }