Repository: eclipse/leshan
Pull request #762 (2019-10-25 13:31:14)

Fix JSON content format implementation
Our current JSON implementation wasn't really respect the LWM2M specification. (see #651)
This should fix issues !

+does not allow JSON format when using create without instance id


Patch:
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 1b8e9aaa..f8da080b 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
@@ -80,18 +80,16 @@ public class LwM2mNodeJsonDecoder {
         }
     }
 
-    private static List<TimestampedLwM2mNode> parseJSON(JsonRootObject jsonObject, LwM2mPath path, LwM2mModel model,
-            Class<? extends LwM2mNode> nodeClass) throws CodecException {
+    private static List<TimestampedLwM2mNode> parseJSON(JsonRootObject jsonObject, LwM2mPath requestPath,
+            LwM2mModel model, Class<? extends LwM2mNode> nodeClass) throws CodecException {
 
-        LOG.trace("Parsing JSON content for path {}: {}", path, jsonObject);
+        LOG.trace("Parsing JSON content for path {}: {}", requestPath, jsonObject);
 
         // Group JSON entry by time-stamp
         Map<Long, Collection<JsonArrayEntry>> jsonEntryByTimestamp = groupJsonEntryByTimestamp(jsonObject);
 
         // Extract baseName
-        LwM2mPath baseName = extractAndValidateBaseName(jsonObject, path);
-        if (baseName == null)
-            baseName = path; // if no base name, use request path as base name
+        String baseName = jsonObject.getBaseName() == null ? "" : jsonObject.getBaseName();
 
         // fill time-stamped nodes collection
         List<TimestampedLwM2mNode> timestampedNodes = new ArrayList<>();
@@ -99,7 +97,7 @@ public class LwM2mNodeJsonDecoder {
 
             // Group JSON entry by instance
             Map<Integer, Collection<JsonArrayEntry>> jsonEntryByInstanceId = groupJsonEntryByInstanceId(
-                    entryByTimestamp.getValue(), baseName);
+                    entryByTimestamp.getValue(), baseName, requestPath);
 
             // Create lwm2m node
             LwM2mNode node;
@@ -107,37 +105,37 @@ public class LwM2mNodeJsonDecoder {
                 Collection<LwM2mObjectInstance> instances = new ArrayList<>();
                 for (Entry<Integer, Collection<JsonArrayEntry>> entryByInstanceId : jsonEntryByInstanceId.entrySet()) {
                     Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(entryByInstanceId.getValue(),
-                            baseName, model);
+                            baseName, model, requestPath);
 
                     instances.add(new LwM2mObjectInstance(entryByInstanceId.getKey(), resourcesMap.values()));
                 }
 
-                node = new LwM2mObject(baseName.getObjectId(), instances);
+                node = new LwM2mObject(requestPath.getObjectId(), instances);
             } else if (nodeClass == LwM2mObjectInstance.class) {
                 // validate we have resources for only 1 instance
                 if (jsonEntryByInstanceId.size() != 1)
-                    throw new CodecException("One instance expected in the payload [path:%s]", path);
+                    throw new CodecException("One instance expected in the payload [path:%s]", requestPath);
 
                 // Extract resources
                 Entry<Integer, Collection<JsonArrayEntry>> instanceEntry = jsonEntryByInstanceId.entrySet().iterator()
                         .next();
                 Map<Integer, LwM2mResource> resourcesMap = extractLwM2mResources(instanceEntry.getValue(), baseName,
-                        model);
+                        model, requestPath);
 
                 // Create instance
                 node = new LwM2mObjectInstance(instanceEntry.getKey(), resourcesMap.values());
             } else if (nodeClass == LwM2mResource.class) {
                 // validate we have resources for only 1 instance
                 if (jsonEntryByInstanceId.size() > 1)
-                    throw new CodecException("Only one instance expected in the payload [path:%s]", path);
+                    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);
+                        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]", path);
+                    throw new CodecException("One resource should be present in the payload [path:%s]", requestPath);
 
                 node = resourcesMap.values().iterator().next();
             } else {
@@ -221,12 +219,12 @@ public class LwM2mNodeJsonDecoder {
      * @return a map (instanceId => collection of JsonArrayEntry)
      */
     private static Map<Integer, Collection<JsonArrayEntry>> groupJsonEntryByInstanceId(
-            Collection<JsonArrayEntry> jsonEntries, LwM2mPath baseName) throws CodecException {
+            Collection<JsonArrayEntry> jsonEntries, String baseName, LwM2mPath requestPath) throws CodecException {
         Map<Integer, Collection<JsonArrayEntry>> result = new HashMap<>();
 
         for (JsonArrayEntry e : jsonEntries) {
             // Build resource path
-            LwM2mPath nodePath = baseName.append(e.getName());
+            LwM2mPath nodePath = extractAndValidatePath(baseName, e.getName() == null ? "" : e.getName(), requestPath);
 
             // Validate path
             if (!nodePath.isResourceInstance() && !nodePath.isResource()) {
@@ -247,45 +245,49 @@ public class LwM2mNodeJsonDecoder {
         }
 
         // Create an entry for an empty instance if possible
-        if (result.isEmpty() && baseName.getObjectInstanceId() != null) {
-            result.put(baseName.getObjectInstanceId(), new ArrayList<JsonArrayEntry>());
+        if (result.isEmpty()) {
+            if (baseName.isEmpty()) {
+                // search object instance id in request path
+                if (requestPath.getObjectInstanceId() != null) {
+                    result.put(requestPath.getObjectInstanceId(), new ArrayList<JsonArrayEntry>());
+                }
+            } else {
+                // search object instance id in basename
+                LwM2mPath basePath = extractAndValidatePath(baseName, "", requestPath);
+                if (basePath.getObjectInstanceId() != null)
+                    result.put(basePath.getObjectInstanceId(), new ArrayList<JsonArrayEntry>());
+            }
         }
         return result;
     }
 
-    private static LwM2mPath extractAndValidateBaseName(JsonRootObject jsonObject, LwM2mPath requestPath)
+    private static LwM2mPath extractAndValidatePath(String baseName, String name, LwM2mPath requestPath)
             throws CodecException {
-        // Check baseName is valid
-        if (jsonObject.getBaseName() != null && !jsonObject.getBaseName().isEmpty()) {
-            LwM2mPath bnPath = new LwM2mPath(jsonObject.getBaseName());
-
-            // check returned base name path is under requested path
-            if (requestPath.getObjectId() != null && bnPath.getObjectId() != null) {
-                if (!bnPath.getObjectId().equals(requestPath.getObjectId())) {
-                    throw new CodecException("Basename path [%s] does not match requested path [%s].", bnPath,
+        LwM2mPath path = new LwM2mPath(baseName + name);
+
+        // check returned path is under requested path
+        if (requestPath.getObjectId() != null && path.getObjectId() != null) {
+            if (!path.getObjectId().equals(requestPath.getObjectId())) {
+                throw new CodecException("resource path [%s] does not match requested path [%s].", path, requestPath);
+            }
+            if (requestPath.getObjectInstanceId() != null && path.getObjectInstanceId() != null) {
+                if (!path.getObjectInstanceId().equals(requestPath.getObjectInstanceId())) {
+                    throw new CodecException("Basename path [%s] does not match requested path [%s].", path,
                             requestPath);
                 }
-                if (requestPath.getObjectInstanceId() != null && bnPath.getObjectInstanceId() != null) {
-                    if (!bnPath.getObjectInstanceId().equals(requestPath.getObjectInstanceId())) {
-                        throw new CodecException("Basename path [%s] does not match requested path [%s].", bnPath,
+                if (requestPath.getResourceId() != null && path.getResourceId() != null) {
+                    if (!path.getResourceId().equals(requestPath.getResourceId())) {
+                        throw new CodecException("Basename path [%s] does not match requested path [%s].", path,
                                 requestPath);
                     }
-                    if (requestPath.getResourceId() != null && bnPath.getResourceId() != null) {
-                        if (!bnPath.getResourceId().equals(requestPath.getResourceId())) {
-                            throw new CodecException("Basename path [%s] does not match requested path [%s].", bnPath,
-                                    requestPath);
-                        }
-                    }
                 }
             }
-            return bnPath;
         }
-        return null;
-
+        return path;
     }
 
     private static Map<Integer, LwM2mResource> extractLwM2mResources(Collection<JsonArrayEntry> jsonArrayEntries,
-            LwM2mPath baseName, LwM2mModel model) throws CodecException {
+            String baseName, LwM2mModel model, LwM2mPath requestPath) throws CodecException {
         if (jsonArrayEntries == null)
             return Collections.emptyMap();
 
@@ -294,8 +296,13 @@ public class LwM2mNodeJsonDecoder {
         Map<LwM2mPath, Map<Integer, JsonArrayEntry>> multiResourceMap = new HashMap<>();
         for (JsonArrayEntry resourceElt : jsonArrayEntries) {
 
-            // Build resource path
-            LwM2mPath nodePath = baseName.append(resourceElt.getName());
+            // Build resource path (path validation was already done in groupJsonEntryByInstanceId
+            LwM2mPath nodePath;
+            if (resourceElt.getName() == null) {
+                nodePath = new LwM2mPath(baseName);
+            } else {
+                nodePath = new LwM2mPath(baseName + resourceElt.getName());
+            }
 
             // handle LWM2M resources
             if (nodePath.isResourceInstance()) {
@@ -356,13 +363,21 @@ public class LwM2mNodeJsonDecoder {
         }
 
         // If we found nothing, we try to create an empty multi-instance resource
-        if (lwM2mResourceMap.isEmpty() && baseName.isResource()) {
-            ResourceModel resourceModel = model.getResourceModel(baseName.getObjectId(), baseName.getResourceId());
-            // We create it only if this respect the model
-            if (resourceModel == null || resourceModel.multiple) {
-                Type resourceType = getResourceType(baseName, model, null);
-                lwM2mResourceMap.put(baseName.getResourceId(), LwM2mMultipleResource
-                        .newResource(baseName.getResourceId(), new HashMap<Integer, Object>(), resourceType));
+        if (lwM2mResourceMap.isEmpty()) {
+            LwM2mPath path;
+            if (baseName.isEmpty()) {
+                path = requestPath;
+            } else {
+                path = extractAndValidatePath(baseName, "", requestPath);
+            }
+            if (path.getObjectId() != null && path.getResourceId() != null) {
+                ResourceModel resourceModel = model.getResourceModel(path.getObjectId(), path.getResourceId());
+                // We create it only if this respect the model
+                if (resourceModel == null || resourceModel.multiple) {
+                    Type resourceType = getResourceType(path, model, null);
+                    lwM2mResourceMap.put(path.getResourceId(), LwM2mMultipleResource.newResource(path.getResourceId(),
+                            new HashMap<Integer, Object>(), resourceType));
+                }
             }
         }
 
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 af976e8e..483f5695 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
@@ -58,7 +58,7 @@ public class LwM2mNodeJsonEncoder {
         node.accept(internalEncoder);
         JsonRootObject jsonObject = new JsonRootObject();
         jsonObject.setResourceList(internalEncoder.resourceList);
-        jsonObject.setBaseName(path.toString());
+        jsonObject.setBaseName(internalEncoder.baseName);
         return LwM2mJson.toJsonLwM2m(jsonObject).getBytes();
     }
 
@@ -70,6 +70,7 @@ public class LwM2mNodeJsonEncoder {
 
         InternalEncoder internalEncoder = new InternalEncoder();
         ArrayList<JsonArrayEntry> entries = new ArrayList<>();
+        String baseName = null;
         for (TimestampedLwM2mNode timestampedLwM2mNode : timestampedNodes) {
             internalEncoder.objectId = path.getObjectId();
             internalEncoder.model = model;
@@ -79,10 +80,19 @@ public class LwM2mNodeJsonEncoder {
             internalEncoder.timestamp = timestampedLwM2mNode.getTimestamp();
             timestampedLwM2mNode.getNode().accept(internalEncoder);
             entries.addAll(internalEncoder.resourceList);
+            if (baseName != null) {
+                if (!baseName.equals(internalEncoder.baseName)) {
+                    throw new CodecException(
+                            "Unexpected baseName %s (%s expected) when encoding timestamped nodes for request %s",
+                            internalEncoder.baseName, baseName, path);
+                }
+            } else {
+                baseName = internalEncoder.baseName;
+            }
         }
         JsonRootObject jsonObject = new JsonRootObject();
         jsonObject.setResourceList(entries);
-        jsonObject.setBaseName(path.toString());
+        jsonObject.setBaseName(internalEncoder.baseName);
         return LwM2mJson.toJsonLwM2m(jsonObject).getBytes();
     }
 
@@ -96,6 +106,7 @@ public class LwM2mNodeJsonEncoder {
 
         // visitor output
         private ArrayList<JsonArrayEntry> resourceList = null;
+        private String baseName = null;
 
         @Override
         public void visit(LwM2mObject object) {
@@ -104,6 +115,7 @@ public class LwM2mNodeJsonEncoder {
             if (!requestPath.isObject()) {
                 throw new CodecException("Invalid request path %s for JSON object encoding", requestPath);
             }
+            baseName = requestPath.toString() + "/";
 
             // Create resources
             resourceList = new ArrayList<>();
@@ -119,6 +131,9 @@ public class LwM2mNodeJsonEncoder {
         public void visit(LwM2mObjectInstance instance) {
             LOG.trace("Encoding object instance {} into JSON", instance);
             resourceList = new ArrayList<>();
+            if (instance.getId() == LwM2mObjectInstance.UNDEFINED) {
+                throw new CodecException("Unable to use JSON format without to give the object instance Id");
+            }
             for (LwM2mResource resource : instance.getResources().values()) {
                 // Validate request path & compute resource path
                 String prefixPath;
@@ -129,6 +144,7 @@ public class LwM2mNodeJsonEncoder {
                 } else {
                     throw new CodecException("Invalid request path %s for JSON instance encoding", requestPath);
                 }
+                baseName = requestPath + "/";
                 // Create resources
                 resourceList.addAll(lwM2mResourceToJsonArrayEntry(prefixPath, timestamp, resource));
             }
@@ -140,8 +156,12 @@ public class LwM2mNodeJsonEncoder {
             if (!requestPath.isResource()) {
                 throw new CodecException("Invalid request path %s for JSON resource encoding", requestPath);
             }
-
-            resourceList = lwM2mResourceToJsonArrayEntry("", timestamp, resource);
+            if (resource.isMultiInstances()) {
+                baseName = requestPath.toString() + "/";
+            } else {
+                baseName = requestPath.toString();
+            }
+            resourceList = lwM2mResourceToJsonArrayEntry(null, timestamp, resource);
         }
 
         private ArrayList<JsonArrayEntry> lwM2mResourceToJsonArrayEntry(String resourcePath, Long timestamp,
@@ -183,7 +203,7 @@ public class LwM2mNodeJsonEncoder {
                 jsonResourceElt.setTime(timestamp);
 
                 // Convert value using expected type
-                LwM2mPath lwM2mResourcePath = new LwM2mPath(resourcePath);
+                LwM2mPath lwM2mResourcePath = resourcePath != null ? new LwM2mPath(resourcePath) : null;
                 this.setResourceValue(converter.convertValue(resource.getValue(), resource.getType(), expectedType,
                         lwM2mResourcePath), expectedType, jsonResourceElt, lwM2mResourcePath);
 
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CreateRequest.java b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CreateRequest.java
index 8fced360..bb03a65e 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/request/CreateRequest.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/request/CreateRequest.java
@@ -232,6 +232,11 @@ public class CreateRequest extends AbstractDownlinkRequest<CreateResponse> {
         this.instanceId = instanceId;
         this.resources = Collections.unmodifiableList(Arrays.asList(resources));
         this.contentFormat = format != null ? format : ContentFormat.TLV; // default to TLV
+
+        if (this.contentFormat == ContentFormat.JSON && instanceId == null) {
+            throw new InvalidRequestException(
+                    "Missing object instance id for CREATE request (%s) using JSON content format.", target);
+        }
     }
 
     @Override
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/json/JsonArrayEntrySerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/json/JsonArrayEntrySerDes.java
index ea17a35b..58993d4a 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/json/JsonArrayEntrySerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/json/JsonArrayEntrySerDes.java
@@ -26,7 +26,8 @@ public class JsonArrayEntrySerDes extends JsonSerDes<JsonArrayEntry> {
     @Override
     public JsonObject jSerialize(JsonArrayEntry jae) {
         JsonObject o = new JsonObject();
-        o.add("n", jae.getName());
+        if (jae.getName() != null)
+            o.add("n", jae.getName());
         Type type = jae.getType();
         if (type != null) {
             switch (jae.getType()) {
@@ -43,7 +44,7 @@ public class JsonArrayEntrySerDes extends JsonSerDes<JsonArrayEntry> {
                 o.add("sv", jae.getStringValue());
                 break;
             default:
-                break;
+                throw new LwM2mJsonException("JsonArrayEntry MUST have a value : %s", jae);
             }
         }
         if (jae.getTime() != null)
@@ -79,6 +80,10 @@ public class JsonArrayEntrySerDes extends JsonSerDes<JsonArrayEntry> {
         if (ov != null && ov.isString())
             jae.setObjectLinkValue(ov.asString());
 
+        if (jae.getType() == null) {
+            throw new LwM2mJsonException("Missing value(v,bv,ov,sv) field for entry %s", o.toString());
+        }
+
         return jae;
     }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/json/JsonRootObjectSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/json/JsonRootObjectSerDes.java
index 76d6e25d..257bc271 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/json/JsonRootObjectSerDes.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/json/JsonRootObjectSerDes.java
@@ -52,6 +52,8 @@ public class JsonRootObjectSerDes extends JsonSerDes<JsonRootObject> {
         JsonValue e = o.get("e");
         if (e != null)
             jro.setResourceList(serDes.deserialize(e.asArray()));
+        else
+            throw new LwM2mJsonException("'e' field is missing for %s", o.toString());
 
         JsonValue bn = o.get("bn");
         if (bn != null && bn.isString())
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/json/LwM2mJsonException.java b/leshan-core/src/main/java/org/eclipse/leshan/json/LwM2mJsonException.java
index 71faff1f..e4440b5b 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/json/LwM2mJsonException.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/json/LwM2mJsonException.java
@@ -19,7 +19,7 @@ package org.eclipse.leshan.json;
 /**
  * Exception thrown in case of JSON parsing error
  */
-public class LwM2mJsonException extends Exception {
+public class LwM2mJsonException extends RuntimeException {
 
     private static final long serialVersionUID = 1L;
 
@@ -27,6 +27,14 @@ public class LwM2mJsonException extends Exception {
         super(message);
     }
 
+    public LwM2mJsonException(String message, Object... args) {
+        super(String.format(message, args));
+    }
+
+    public LwM2mJsonException(Exception e, String message, Object... args) {
+        super(String.format(message, args), e);
+    }
+
     public LwM2mJsonException(String message, Exception cause) {
         super(message, cause);
     }


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 1727f40a..665f73d3 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
@@ -39,6 +39,7 @@ import org.eclipse.leshan.core.node.LwM2mSingleResource;
 import org.eclipse.leshan.core.node.ObjectLink;
 import org.eclipse.leshan.core.node.TimestampedLwM2mNode;
 import org.eclipse.leshan.core.request.ContentFormat;
+import org.eclipse.leshan.json.LwM2mJsonException;
 import org.eclipse.leshan.tlv.Tlv;
 import org.eclipse.leshan.tlv.Tlv.TlvType;
 import org.eclipse.leshan.tlv.TlvEncoder;
@@ -243,8 +244,8 @@ public class LwM2mNodeDecoderTest {
     @Test(expected = CodecException.class)
     public void text_decode_should_throw_an_exception_for_invalid_base64() throws CodecException {
         // Using Firmware Update/Package
-        decoder.decode("!,-INVALID$_'".getBytes(StandardCharsets.UTF_8),
-                ContentFormat.TEXT, new LwM2mPath(5, 0, 0), model);
+        decoder.decode("!,-INVALID$_'".getBytes(StandardCharsets.UTF_8), ContentFormat.TEXT, new LwM2mPath(5, 0, 0),
+                model);
     }
 
     @Test
@@ -461,7 +462,8 @@ public class LwM2mNodeDecoderTest {
     public void json_device_object_instance0() throws CodecException {
         // json content for instance 0 of device object
         StringBuilder b = new StringBuilder();
-        b.append("{\"e\":[");
+        b.append("{\"bn\":\"/3/0/\",");
+        b.append("\"e\":[");
         b.append("{\"n\":\"0\",\"sv\":\"Open Mobile Alliance\"},");
         b.append("{\"n\":\"1\",\"sv\":\"Lightweight M2M Client\"},");
         b.append("{\"n\":\"2\",\"sv\":\"345000123\"},");
@@ -486,7 +488,7 @@ public class LwM2mNodeDecoderTest {
     }
 
     @Test
-    public void json_device_object_instance0_with_root_basename() throws CodecException {
+    public void json_device_object_instance0_with_empty_root_basename() throws CodecException {
         // json content for instance 0 of device object
         StringBuilder b = new StringBuilder();
         b.append("{\"bn\":\"/\",");
@@ -515,10 +517,38 @@ public class LwM2mNodeDecoderTest {
     }
 
     @Test
-    public void json_custom_object_instance() throws CodecException {
+    public void json_device_object_instance0_without_basename() throws CodecException {
         // json content for instance 0 of device object
         StringBuilder b = new StringBuilder();
         b.append("{\"e\":[");
+        b.append("{\"n\":\"/3/0/0\",\"sv\":\"Open Mobile Alliance\"},");
+        b.append("{\"n\":\"/3/0/1\",\"sv\":\"Lightweight M2M Client\"},");
+        b.append("{\"n\":\"/3/0/2\",\"sv\":\"345000123\"},");
+        b.append("{\"n\":\"/3/0/3\",\"sv\":\"1.0\"},");
+        b.append("{\"n\":\"/3/0/6/0\",\"v\":1},");
+        b.append("{\"n\":\"/3/0/6/1\",\"v\":5},");
+        b.append("{\"n\":\"/3/0/7/0\",\"v\":3800},");
+        b.append("{\"n\":\"/3/0/7/1\",\"v\":5000},");
+        b.append("{\"n\":\"/3/0/8/0\",\"v\":125},");
+        b.append("{\"n\":\"/3/0/8/1\",\"v\":900},");
+        b.append("{\"n\":\"/3/0/9\",\"v\":100},");
+        b.append("{\"n\":\"/3/0/10\",\"v\":15},");
+        b.append("{\"n\":\"/3/0/11/0\",\"v\":0},");
+        b.append("{\"n\":\"/3/0/13\",\"v\":1367491215},");
+        b.append("{\"n\":\"/3/0/14\",\"sv\":\"+02:00\"},");
+        b.append("{\"n\":\"/3/0/16\",\"sv\":\"U\"}]}");
+
+        LwM2mObjectInstance oInstance = (LwM2mObjectInstance) decoder.decode(b.toString().getBytes(),
+                ContentFormat.JSON, new LwM2mPath(3, 0), model);
+
+        assertDeviceInstance(oInstance);
+    }
+
+    @Test
+    public void json_custom_object_instance() throws CodecException {
+        // json content for instance 0 of device object
+        StringBuilder b = new StringBuilder();
+        b.append("{\"bn\":\"1024/0/\", \"e\":[");
         b.append("{\"n\":\"0\",\"sv\":\"a string\"},");
         b.append("{\"n\":\"1\",\"v\":10.5},");
         b.append("{\"n\":\"2\",\"bv\":true}]}");
@@ -536,10 +566,10 @@ public class LwM2mNodeDecoderTest {
     public void json_timestamped_resources() throws CodecException {
         // json content for instance 0 of device object
         StringBuilder b = new StringBuilder();
-        b.append("{\"e\":[");
-        b.append("{\"n\":\"\",\"v\":22.9,\"t\":-30},");
-        b.append("{\"n\":\"\",\"v\":22.4,\"t\":-5},");
-        b.append("{\"n\":\"\",\"v\":24.1,\"t\":-50}],");
+        b.append("{\"bn\":\"/1024/0/1\",\"e\":[");
+        b.append("{\"v\":22.9,\"t\":-30},");
+        b.append("{\"v\":22.4,\"t\":-5},");
+        b.append("{\"v\":24.1,\"t\":-50}],");
         b.append("\"bt\":25462634}");
 
         List<TimestampedLwM2mNode> timestampedResources = decoder.decodeTimestampedData(b.toString().getBytes(),
@@ -558,7 +588,7 @@ public class LwM2mNodeDecoderTest {
     public void json_timestamped_instances() throws CodecException {
         // json content for instance 0 of device object
         StringBuilder b = new StringBuilder();
-        b.append("{\"e\":[");
+        b.append("{\"bn\":\"/1024/0/\",\"e\":[");
         b.append("{\"n\":\"1\",\"v\":22.9,\"t\":-30},");
         b.append("{\"n\":\"1\",\"v\":22.4,\"t\":-5},");
         b.append("{\"n\":\"0\",\"sv\":\"a string\",\"t\":-5},");
@@ -585,7 +615,7 @@ public class LwM2mNodeDecoderTest {
     public void json_timestamped_Object() throws CodecException {
         // json content for instance 0 of device object
         StringBuilder b = new StringBuilder();
-        b.append("{\"e\":[");
+        b.append("{\"bn\":\"/1024/\",\"e\":[");
         b.append("{\"n\":\"0/1\",\"v\":22.9,\"t\":-30},");
         b.append("{\"n\":\"0/1\",\"v\":22.4,\"t\":-5},");
         b.append("{\"n\":\"0/0\",\"sv\":\"a string\",\"t\":-5},");
@@ -615,13 +645,17 @@ public class LwM2mNodeDecoderTest {
     @Test
     public void json_empty_object() {
         // Completely empty
+        LwM2mObject obj = null;
         StringBuilder b = new StringBuilder();
         b.append("{}");
-        LwM2mObject obj = (LwM2mObject) decoder.decode(b.toString().getBytes(), ContentFormat.JSON, new LwM2mPath(2),
-                model);
-        assertNotNull(obj);
-        assertEquals(2, obj.getId());
-        assertTrue(obj.getInstances().isEmpty());
+        boolean failedWithCodecException = false;
+        try {
+            obj = (LwM2mObject) decoder.decode(b.toString().getBytes(), ContentFormat.JSON, new LwM2mPath(2), model);
+        } catch (CodecException e) {
+            assertTrue(e.getCause() instanceof LwM2mJsonException);
+            failedWithCodecException = true;
+        }
+        assertTrue("Should failed with codec exception", failedWithCodecException);
 
         // with empty resource list
         b = new StringBuilder();
@@ -643,13 +677,18 @@ public class LwM2mNodeDecoderTest {
     @Test
     public void json_empty_instance() {
         // Completely empty
+        LwM2mObjectInstance instance = null;
         StringBuilder b = new StringBuilder();
         b.append("{}");
-        LwM2mObjectInstance instance = (LwM2mObjectInstance) decoder.decode(b.toString().getBytes(), ContentFormat.JSON,
-                new LwM2mPath(2, 0), model);
-        assertNotNull(instance);
-        assertEquals(0, instance.getId());
-        assertTrue(instance.getResources().isEmpty());
+        boolean failedWithCodecException = false;
+        try {
+            instance = (LwM2mObjectInstance) decoder.decode(b.toString().getBytes(), ContentFormat.JSON,
+                    new LwM2mPath(2, 0), model);
+        } catch (CodecException e) {
+            assertTrue(e.getCause() instanceof LwM2mJsonException);
+            failedWithCodecException = true;
+        }
+        assertTrue("Should failed with codec exception", failedWithCodecException);
 
         // with empty resource list
         b = new StringBuilder();
@@ -673,7 +712,7 @@ public class LwM2mNodeDecoderTest {
     @Test(expected = CodecException.class)
     public void json_empty_single_resource() {
         StringBuilder b = new StringBuilder();
-        b.append("{}");
+        b.append("{\"bn\":\"2/0/0\", \"e\":[]}");
 
         decoder.decode(b.toString().getBytes(), ContentFormat.JSON, new LwM2mPath(2, 0, 0), model);
     }
@@ -681,14 +720,18 @@ public class LwM2mNodeDecoderTest {
     @Test
     public void json_empty_multi_resource() {
         // Completely empty
+        LwM2mResource resource = null;
         StringBuilder b = new StringBuilder();
         b.append("{}");
-        LwM2mResource resource = (LwM2mResource) decoder.decode(b.toString().getBytes(), ContentFormat.JSON,
-                new LwM2mPath(3, 0, 6), model);
-        assertNotNull(resource);
-        assertTrue(resource instanceof LwM2mMultipleResource);
-        assertEquals(6, resource.getId());
-        assertTrue(resource.getValues().isEmpty());
+        boolean failedWithCodecException = false;
+        try {
+            resource = (LwM2mResource) decoder.decode(b.toString().getBytes(), ContentFormat.JSON,
+                    new LwM2mPath(3, 0, 6), model);
+        } catch (CodecException e) {
+            assertTrue(e.getCause() instanceof LwM2mJsonException);
+            failedWithCodecException = true;
+        }
+        assertTrue("Should failed with codec exception", failedWithCodecException);
 
         // with empty resource list
         b = new StringBuilder();
@@ -711,10 +754,25 @@ public class LwM2mNodeDecoderTest {
         assertTrue(resource.getValues().isEmpty());
     }
 
+    @Test
+    public void json_missing_value_for_resource() {
+
+        StringBuilder b = new StringBuilder();
+        b.append("{\"bn\":\"2/0/\",\"e\":[");
+        b.append("{\"n\":\"0\"}");
+        b.append("]}");
+        try {
+            decoder.decode(b.toString().getBytes(), ContentFormat.JSON, new LwM2mPath(2, 0, 0), model);
+            fail();
+        } catch (CodecException e) {
+            assertTrue(e.getCause() instanceof LwM2mJsonException);
+        }
+    }
+
     @Test(expected = CodecException.class)
     public void json_invalid_instance_2_resources_with_the_same_id() {
         StringBuilder b = new StringBuilder();
-        b.append("{\"e\":[");
+        b.append("{\"bn\":\"3/0/\",\"e\":[");
         b.append("{\"n\":\"1\",\"sv\":\"client1\"},");
         b.append("{\"n\":\"1\",\"sv\":\"client2\"}");
         b.append("]}");
@@ -725,11 +783,27 @@ public class LwM2mNodeDecoderTest {
     @Test(expected = CodecException.class)
     public void json_invalid_multi_resource_2_instances_with_the_same_id() {
         StringBuilder b = new StringBuilder();
-        b.append("{\"e\":[");
+        b.append("{\"bn\":\"3/0/11/\",\"e\":[");
         b.append("{\"n\":\"1\",\"v\":2},");
         b.append("{\"n\":\"1\",\"v\":0}");
         b.append("]}");
 
         decoder.decode(b.toString().getBytes(), ContentFormat.JSON, new LwM2mPath(3, 0, 11), model);
     }
+
+    @Test
+    public void json_cut_basename_name_in_the_middle_of_an_id() {
+        StringBuilder b = new StringBuilder();
+        b.append("{\"bn\":\"3/0/1\",\"e\":[");
+        b.append("{\"n\":\"1/0\",\"v\":0},");
+        b.append("{\"n\":\"1/1\",\"v\":0}");
+        b.append("]}");
+
+        LwM2mResource resource = (LwM2mResource) decoder.decode(b.toString().getBytes(), ContentFormat.JSON,
+                new LwM2mPath(3, 0, 11), model);
+        assertNotNull(resource);
+        assertTrue(resource instanceof LwM2mMultipleResource);
+        assertEquals(11, resource.getId());
+        assertTrue(resource.getValues().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 a295a2dc..06eb19c2 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
@@ -180,7 +180,7 @@ public class LwM2mNodeEncoderTest {
         byte[] encoded = encoder.encode(oInstance, ContentFormat.JSON, new LwM2mPath("/3/0"), model);
 
         StringBuilder b = new StringBuilder();
-        b.append("{\"bn\":\"/3/0\",\"e\":[");
+        b.append("{\"bn\":\"/3/0/\",\"e\":[");
         b.append("{\"n\":\"0\",\"sv\":\"Open Mobile Alliance\"},");
         b.append("{\"n\":\"1\",\"sv\":\"Lightweight M2M Client\"},");
         b.append("{\"n\":\"2\",\"sv\":\"345000123\"},");
@@ -213,9 +213,9 @@ public class LwM2mNodeEncoderTest {
 
         StringBuilder b = new StringBuilder();
         b.append("{\"bn\":\"/1024/0/1\",\"e\":[");
-        b.append("{\"n\":\"\",\"v\":22.9,\"t\":500},");
-        b.append("{\"n\":\"\",\"v\":22.4,\"t\":510},");
-        b.append("{\"n\":\"\",\"v\":24.1,\"t\":520}]}");
+        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));
@@ -237,7 +237,7 @@ public class LwM2mNodeEncoderTest {
         byte[] encoded = encoder.encodeTimestampedData(data, ContentFormat.JSON, new LwM2mPath(1024, 0), model);
 
         StringBuilder b = new StringBuilder();
-        b.append("{\"bn\":\"/1024/0\",\"e\":[");
+        b.append("{\"bn\":\"/1024/0/\",\"e\":[");
         b.append("{\"n\":\"1\",\"v\":22.9,\"t\":110},");
         b.append("{\"n\":\"0\",\"sv\":\"a string\",\"t\":120},");
         b.append("{\"n\":\"1\",\"v\":22.4,\"t\":120},");
@@ -269,7 +269,7 @@ public class LwM2mNodeEncoderTest {
         byte[] encoded = encoder.encodeTimestampedData(data, ContentFormat.JSON, new LwM2mPath(1024), model);
 
         StringBuilder b = new StringBuilder();
-        b.append("{\"bn\":\"/1024\",\"e\":[");
+        b.append("{\"bn\":\"/1024/\",\"e\":[");
         b.append("{\"n\":\"0/1\",\"v\":22.9,\"t\":210},");
         b.append("{\"n\":\"0/0\",\"sv\":\"a string\",\"t\":220},");
         b.append("{\"n\":\"0/1\",\"v\":22.4,\"t\":220},");