Repository: eclipse/leshan
Pull request #937 (2020-11-25 17:11:06)

Add SenML-CBOR Encoder/Decoder based on upokecenter CBOR-Java and use it by default
This PR follow #922.


Patch:
diff --git a/leshan-core/pom.xml b/leshan-core/pom.xml
index 57ecbc1c..abfebcf7 100644
--- a/leshan-core/pom.xml
+++ b/leshan-core/pom.xml
@@ -46,6 +46,10 @@ Contributors:
             <groupId>com.fasterxml.jackson.dataformat</groupId>
             <artifactId>jackson-dataformat-cbor</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.upokecenter</groupId>
+            <artifactId>cbor</artifactId>
+        </dependency>
 
         <!-- test dependencies -->
         <dependency>
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 07ba6059..0a0f13f5 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
@@ -37,7 +37,7 @@ import org.eclipse.leshan.core.node.codec.text.LwM2mNodeTextDecoder;
 import org.eclipse.leshan.core.node.codec.tlv.LwM2mNodeTlvDecoder;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.util.Validate;
-import org.eclipse.leshan.senml.cbor.jackson.SenMLCborJacksonEncoderDecoder;
+import org.eclipse.leshan.senml.cbor.upokecenter.SenMLCborUpokecenterEncoderDecoder;
 import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -60,7 +60,7 @@ public class DefaultLwM2mNodeDecoder implements LwM2mNodeDecoder {
         decoders.put(ContentFormat.TEXT, new LwM2mNodeTextDecoder());
         decoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueDecoder());
         decoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLDecoder(new SenMLJsonJacksonEncoderDecoder()));
-        decoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLDecoder(new SenMLCborJacksonEncoderDecoder()));
+        decoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLDecoder(new SenMLCborUpokecenterEncoderDecoder()));
 
         // tlv
         LwM2mNodeTlvDecoder tlvDecoder = new LwM2mNodeTlvDecoder();
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
index b2ff3075..16aeb034 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/node/codec/DefaultLwM2mNodeEncoder.java
@@ -31,7 +31,7 @@ import org.eclipse.leshan.core.node.codec.text.LwM2mNodeTextEncoder;
 import org.eclipse.leshan.core.node.codec.tlv.LwM2mNodeTlvEncoder;
 import org.eclipse.leshan.core.request.ContentFormat;
 import org.eclipse.leshan.core.util.Validate;
-import org.eclipse.leshan.senml.cbor.jackson.SenMLCborJacksonEncoderDecoder;
+import org.eclipse.leshan.senml.cbor.upokecenter.SenMLCborUpokecenterEncoderDecoder;
 import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -54,7 +54,7 @@ public class DefaultLwM2mNodeEncoder implements LwM2mNodeEncoder {
         encoders.put(ContentFormat.TEXT, new LwM2mNodeTextEncoder());
         encoders.put(ContentFormat.OPAQUE, new LwM2mNodeOpaqueEncoder());
         encoders.put(ContentFormat.SENML_JSON, new LwM2mNodeSenMLEncoder(new SenMLJsonJacksonEncoderDecoder()));
-        encoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLEncoder(new SenMLCborJacksonEncoderDecoder()));
+        encoders.put(ContentFormat.SENML_CBOR, new LwM2mNodeSenMLEncoder(new SenMLCborUpokecenterEncoderDecoder()));
 
         // tlv
         LwM2mNodeTlvEncoder tlvDecoder = new LwM2mNodeTlvEncoder();
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
new file mode 100644
index 00000000..334d049d
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborPackSerDes.java
@@ -0,0 +1,197 @@
+/*******************************************************************************
+ * 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.senml.cbor.upokecenter;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Collection;
+
+import org.eclipse.leshan.core.model.ResourceModel.Type;
+import org.eclipse.leshan.core.util.Base64;
+import org.eclipse.leshan.core.util.datatype.ULong;
+import org.eclipse.leshan.core.util.json.JsonException;
+import org.eclipse.leshan.senml.SenMLException;
+import org.eclipse.leshan.senml.SenMLPack;
+import org.eclipse.leshan.senml.SenMLRecord;
+
+import com.upokecenter.cbor.CBORObject;
+import com.upokecenter.cbor.CBORType;
+import com.upokecenter.numbers.EInteger;
+
+public class SenMLCborPackSerDes {
+
+    public SenMLPack deserializeFromCbor(Collection<CBORObject> objects) throws SenMLException {
+        try {
+            SenMLPack senMLPack = new SenMLPack();
+            for (CBORObject o : objects) {
+                SenMLRecord record = new SenMLRecord();
+
+                CBORObject bn = o.get(-2);
+                if (bn != null && bn.getType() == CBORType.TextString)
+                    record.setBaseName(bn.AsString());
+
+                CBORObject bt = o.get(-3);
+                if (bt != null && bt.isNumber())
+                    record.setBaseTime(bt.AsInt64());
+
+                CBORObject n = o.get(0);
+                if (n != null && n.getType() == CBORType.TextString)
+                    record.setName(n.AsString());
+
+                CBORObject t = o.get(6);
+                if (t != null && t.isNumber())
+                    record.setTime(t.AsInt64());
+
+                CBORObject v = o.get(2);
+                boolean hasValue = false;
+                if (v != null && v.isNumber()) {
+                    if (v.isIntegral()) {
+                        if (v.isNegative()) {
+                            record.setFloatValue(v.AsInt64());
+                        } else {
+                            EInteger eInteger = v.AsEInteger();
+                            if (eInteger.CanFitInInt64()) {
+                                record.setFloatValue(eInteger.ToInt64Unchecked());
+                            } else {
+                                // There is maybe a better way to do that.
+                                record.setFloatValue(ULong.valueOf(v.AsEInteger().toString()));
+                            }
+                        }
+                    } else {
+                        record.setFloatValue(v.AsDoubleValue());
+                    }
+                    hasValue = true;
+                }
+
+                CBORObject vb = o.get(4);
+                if (vb != null && vb.getType() == CBORType.Boolean) {
+                    record.setBooleanValue(vb.AsBoolean());
+                    hasValue = true;
+                }
+
+                CBORObject vs = o.get(3);
+                if (vs != null && vs.getType() == CBORType.TextString) {
+                    record.setStringValue(vs.AsString());
+                    hasValue = true;
+                }
+
+                CBORObject vlo = o.get("vlo");
+                if (vlo != null && vlo.getType() == CBORType.TextString) {
+                    record.setObjectLinkValue(vlo.AsString());
+                    hasValue = true;
+                }
+
+                CBORObject vd = o.get(8);
+                if (vd != null && vd.getType() == CBORType.TextString) {
+                    record.setOpaqueValue(Base64.decodeBase64(vd.AsString()));
+                    hasValue = true;
+                }
+
+                if (!hasValue)
+                    throw new JsonException("Invalid SenML record : record must have a value (v,vb,vlo,vd,vs) : %s", o);
+
+                senMLPack.addRecord(record);
+            }
+            return senMLPack;
+        } catch (IllegalArgumentException | IllegalStateException e) {
+            throw new SenMLException(e, "Unable to serialize SenML in CBOR");
+        }
+
+    }
+
+    public byte[] serializeToCbor(SenMLPack pack) throws SenMLException {
+        try (OutputStream stream = new ByteArrayOutputStream()) {
+            CBORObject cborArray = CBORObject.NewArray();
+            for (SenMLRecord record : pack.getRecords()) {
+                CBORObject cborRecord = newMap();
+                if (record.getBaseName() != null && !record.getBaseName().isEmpty()) {
+                    cborRecord.Add(-2, record.getBaseName());
+                }
+
+                if (record.getBaseTime() != null) {
+                    cborRecord.Add(-3, record.getBaseTime());
+                }
+
+                if (record.getName() != null && !record.getName().isEmpty()) {
+                    cborRecord.Add(0, record.getName());
+                }
+
+                if (record.getTime() != null) {
+                    cborRecord.Add(6, record.getTime());
+                }
+
+                Type type = record.getType();
+                if (type != null) {
+                    switch (record.getType()) {
+                    case FLOAT:
+                        Number value = record.getFloatValue();
+                        if (value instanceof Byte) {
+                            cborRecord.Add(2, value.byteValue());
+                        } else if (value instanceof Short) {
+                            cborRecord.Add(2, value.shortValue());
+                        } else if (value instanceof Integer) {
+                            cborRecord.Add(2, value.intValue());
+                        } else if (value instanceof Long) {
+                            cborRecord.Add(2, value.longValue());
+                        } else if (value instanceof BigInteger) {
+                            // probably not supported, not tested
+                            cborRecord.Add(2, value);
+                        }
+                        // unsigned integer
+                        else if (value instanceof ULong) {
+                            // There is maybe a better way to do that.
+                            cborRecord.Add(2, EInteger.FromString(value.toString()));
+                        }
+                        // floating-point
+                        else if (value instanceof Float) {
+                            cborRecord.Add(2, value.floatValue());
+                        } else if (value instanceof Double) {
+                            cborRecord.Add(2, value.doubleValue());
+                        } else if (value instanceof BigDecimal) {
+                            // probably not supported, not tested
+                            cborRecord.Add(2, value);
+                        }
+                        break;
+                    case BOOLEAN:
+                        cborRecord.Add(4, record.getBooleanValue());
+                        break;
+                    case OBJLNK:
+                        cborRecord.Add("vlo", record.getObjectLinkValue());
+                        break;
+                    case OPAQUE:
+                        cborRecord.Add(8, record.getOpaqueValue());
+                    case STRING:
+                        cborRecord.Add(3, record.getStringValue());
+                        break;
+                    default:
+                        break;
+                    }
+                }
+                cborArray.Add(cborRecord);
+            }
+            return cborArray.EncodeToBytes();
+        } catch (IllegalArgumentException | IllegalStateException | IOException e) {
+            throw new SenMLException(e, "Unable to serialize SenML in CBOR");
+        }
+    }
+
+    CBORObject newMap() {
+        return CBORObject.NewMap();
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborUpokecenterEncoderDecoder.java b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborUpokecenterEncoderDecoder.java
new file mode 100644
index 00000000..c82053cb
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/senml/cbor/upokecenter/SenMLCborUpokecenterEncoderDecoder.java
@@ -0,0 +1,92 @@
+/*******************************************************************************
+ * 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.senml.cbor.upokecenter;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.LinkedHashMap;
+
+import org.eclipse.leshan.senml.SenMLDecoder;
+import org.eclipse.leshan.senml.SenMLEncoder;
+import org.eclipse.leshan.senml.SenMLException;
+import org.eclipse.leshan.senml.SenMLPack;
+
+import com.upokecenter.cbor.CBORException;
+import com.upokecenter.cbor.CBORObject;
+import com.upokecenter.cbor.CBORType;
+
+/**
+ * Helper for encoding/decoding SenML CBOR using <a href="https://github.com/peteroupc/CBOR-Java">"upokecenter"
+ * CBOR-Java</a>
+ */
+public class SenMLCborUpokecenterEncoderDecoder implements SenMLDecoder, SenMLEncoder {
+    private final SenMLCborPackSerDes serDes;
+
+    public SenMLCborUpokecenterEncoderDecoder() {
+        this(false);
+    }
+
+    /**
+     * @param keepingInsertionOrder Set it to true allows to keep insertion order at serialization. This is a kind of
+     *        HACK using reflection which could make testing easier but could bring performance penalty
+     * 
+     * @see <a href="https://github.com/peteroupc/CBOR-Java/issues/13">CBOR-Java#13 issue</a>
+     */
+    public SenMLCborUpokecenterEncoderDecoder(boolean keepingInsertionOrder) {
+        if (keepingInsertionOrder) {
+            try {
+                final Constructor<CBORObject> constructor = CBORObject.class.getDeclaredConstructor(int.class,
+                        Object.class);
+                constructor.setAccessible(true);
+                serDes = new SenMLCborPackSerDes() {
+                    @Override
+                    CBORObject newMap() {
+                        try {
+                            return constructor.newInstance(5, new LinkedHashMap<CBORObject, CBORObject>());
+                        } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
+                            throw new IllegalStateException(
+                                    "Unable to use Java reflection to create CBORMap keeping insertion order", e);
+                        }
+                    }
+                };
+            } catch (NoSuchMethodException | SecurityException e) {
+                throw new IllegalStateException("Unable to use Java reflection to keep insertion order", e);
+            }
+        } else {
+            serDes = new SenMLCborPackSerDes();
+        }
+    }
+
+    @Override
+    public byte[] toSenML(SenMLPack pack) throws SenMLException {
+        if (pack == null)
+            return null;
+        return serDes.serializeToCbor(pack);
+    }
+
+    @Override
+    public SenMLPack fromSenML(byte[] data) throws SenMLException {
+        try {
+            CBORObject cborObject = CBORObject.DecodeFromBytes(data);
+            if (cborObject.getType() != CBORType.Array) {
+                throw new SenMLException("Unable to parse SenML CBOR: Array expected but was %s", cborObject.getType());
+            }
+            return serDes.deserializeFromCbor(cborObject.getValues());
+        } catch (CBORException e) {
+            throw new SenMLException("Unable to parse SenML CBOR.", e);
+        }
+    }
+}
diff --git a/pom.xml b/pom.xml
index 808e7925..198dde54 100644
--- a/pom.xml
+++ b/pom.xml
@@ -703,6 +703,11 @@ Contributors:
                 <artifactId>jackson-dataformat-cbor</artifactId>
                 <version>2.11.2</version>
             </dependency>
+            <dependency>
+                <groupId>com.upokecenter</groupId>
+                <artifactId>cbor</artifactId>
+                <version>4.0.0</version>
+            </dependency>
             
 
             <!-- Demos, examples and tests dependencies -->


Test patch:
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
index 421e3a89..d1e7b811 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/AbstractSenMLTest.java
@@ -18,18 +18,19 @@ import org.eclipse.leshan.senml.SenMLPack;
 import org.eclipse.leshan.senml.SenMLRecord;
 
 public abstract class AbstractSenMLTest {
-    protected void givenResourceWithFloatValue(SenMLPack pack, String n, Number value) {
+
+    private void givenResourceWithFloatValue(SenMLPack pack, String n, Number value) {
         SenMLRecord elt = new SenMLRecord();
         elt.setName(n);
         elt.setFloatValue(value);
         pack.addRecord(elt);
     }
 
-    protected void givenResourceWithStringValue(SenMLPack pack, String n, String value) {
+    private void givenResourceWithStringValue(SenMLPack pack, String n, String value) {
         givenResourceWithStringValue(pack, null, n, value);
     }
 
-    protected void givenResourceWithStringValue(SenMLPack pack, String bn, String n, String value) {
+    private void givenResourceWithStringValue(SenMLPack pack, String bn, String n, String value) {
         SenMLRecord elt = new SenMLRecord();
         if (bn != null) {
             elt.setBaseName(bn);
@@ -57,12 +58,24 @@ public abstract class AbstractSenMLTest {
         b.append("{\"n\":\"9\",\"v\":100},");
         b.append("{\"n\":\"10\",\"v\":15},");
         b.append("{\"n\":\"11/0\",\"v\":0},");
-        b.append("{\"n\":\"13\",\"v\":1.3674912E9},");
+        b.append("{\"n\":\"13\",\"v\":1367491215},");
         b.append("{\"n\":\"14\",\"vs\":\"+02:00\"},");
         b.append("{\"n\":\"16\",\"vs\":\"U\"}]");
         return b.toString();
     }
 
+    /**
+     * Example of CBOR payload request to Device Object of the LwM2M example client (Read /3/0)
+     * 
+     * @return JSON payload
+     */
+    protected String givenSenMLCborExample() {
+        return "90a321652f332f302f00613003744f70656e204d6f62696c6520416c6c69616e6365a200613103764c696768747765696768"
+                + "74204d324d20436c69656e74a20061320369333435303030313233a20061330363312e30a20063362f300201a20063362f310205a20063372f"
+                + "3002190ed8a20063372f3102191388a20063382f3002187da20063382f3102190384a2006139021864a200623130020fa2006431312f300200"
+                + "a200623133021a5182428fa20062313403662b30323a3030a200623136036155";
+    }
+
     protected SenMLPack givenDeviceObjectInstance() {
         SenMLPack pack = new SenMLPack();
 
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
index be855bcc..0cd712f2 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLCborSerializerTest.java
@@ -14,34 +14,57 @@
 
 package org.eclipse.leshan.core.senml.cbor;
 
+import java.util.Arrays;
+import java.util.Collection;
+
 import org.eclipse.leshan.core.util.Hex;
+import org.eclipse.leshan.senml.SenMLDecoder;
+import org.eclipse.leshan.senml.SenMLEncoder;
+import org.eclipse.leshan.senml.SenMLException;
 import org.eclipse.leshan.senml.SenMLPack;
 import org.eclipse.leshan.senml.cbor.jackson.SenMLCborJacksonEncoderDecoder;
+import org.eclipse.leshan.senml.cbor.upokecenter.SenMLCborUpokecenterEncoderDecoder;
 import org.junit.Assert;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class SenMLCborSerializerTest extends AbstractSenMLTest {
 
-    @Test
-    public void serialize_device_object_to_senml_cbor() throws Exception {
-        byte[] cbor = new SenMLCborJacksonEncoderDecoder().toSenML(givenDeviceObjectInstance());
+    @Parameterized.Parameters(name = "{2}")
+    public static Collection<?> senMLJsonencoderDecoder() {
+        SenMLCborUpokecenterEncoderDecoder cborJava = new SenMLCborUpokecenterEncoderDecoder(true);
+        SenMLCborJacksonEncoderDecoder jackson = new SenMLCborJacksonEncoderDecoder();
+        return Arrays.asList(new Object[][] { //
+                                { cborJava, cborJava, "cbor-java" }, //
+                                { jackson, jackson, "jackson" } });
+    }
 
-        String expectedValue = "90a321652f332f302f00613003744f70656e204d6f62696c6520416c6c69616e6365a200613103764c696768747765696768"
-                + "74204d324d20436c69656e74a20061320369333435303030313233a20061330363312e30a20063362f300201a20063362f310205a20063372f"
-                + "3002190ed8a20063372f3102191388a20063382f3002187da20063382f3102190384a2006139021864a200623130020fa2006431312f300200"
-                + "a200623133021a5182428fa20062313403662b30323a3030a200623136036155";
+    private SenMLEncoder encoder;
+    private SenMLDecoder decoder;
+
+    public SenMLCborSerializerTest(SenMLEncoder encoder, SenMLDecoder decoder, String encoderDecoderName) {
+        this.encoder = encoder;
+        this.decoder = decoder;
+    }
 
-        Assert.assertTrue(expectedValue.equals(Hex.encodeHexString(cbor)));
+    @Test
+    public void serialize_then_deserialize_device_object() throws SenMLException {
+        byte[] cbor = encoder.toSenML(givenDeviceObjectInstance());
+        SenMLPack pack = decoder.fromSenML(cbor);
+        SenMLTestUtil.assertSenMLPackEquals(givenDeviceObjectInstance(), pack);
+    }
+
+    @Test
+    public void serialize_device_object() throws Exception {
+        byte[] cbor = encoder.toSenML(givenDeviceObjectInstance());
+        Assert.assertEquals(givenSenMLCborExample(), Hex.encodeHexString(cbor));
     }
 
     @Test
-    public void deserialize_device_object_to_senml_cbor() throws Exception {
-        String inputValue = "90a321652f332f302f00613003744f70656e204d6f62696c6520416c6c69616e6365a200613103764c696768747765696768"
-                + "74204d324d20436c69656e74a20061320369333435303030313233a20061330363312e30a20063362f300201a20063362f310205a20063372f"
-                + "3002190ed8a20063372f3102191388a20063382f3002187da20063382f3102190384a2006139021864a200623130020fa2006431312f300200"
-                + "a200623133021a5182428fa20062313403662b30323a3030a200623136036155";
-
-        SenMLPack pack = new SenMLCborJacksonEncoderDecoder().fromSenML(Hex.decodeHex(inputValue.toCharArray()));
-        Assert.assertEquals(givenDeviceObjectInstance(), pack);
+    public void deserialize_device_object_() throws Exception {
+        SenMLPack pack = decoder.fromSenML(Hex.decodeHex(givenSenMLCborExample().toCharArray()));
+        SenMLTestUtil.assertSenMLPackEquals(givenDeviceObjectInstance(), pack);
     }
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java
index cf77545c..dc5068c7 100644
--- a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLJsonSerDesTest.java
@@ -14,6 +14,8 @@
 
 package org.eclipse.leshan.core.senml.cbor;
 
+import static org.junit.Assert.assertEquals;
+
 import java.util.Arrays;
 import java.util.Collection;
 
@@ -23,7 +25,6 @@ import org.eclipse.leshan.senml.SenMLException;
 import org.eclipse.leshan.senml.SenMLPack;
 import org.eclipse.leshan.senml.json.jackson.SenMLJsonJacksonEncoderDecoder;
 import org.eclipse.leshan.senml.json.minimaljson.SenMLJsonMinimalEncoderDecoder;
-import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -49,11 +50,29 @@ public class SenMLJsonSerDesTest extends AbstractSenMLTest {
     }
 
     @Test
-    public void deserialize_device_object_with_minimalJson() throws SenMLException {
+    public void serialize_then_deserialize_device_object() throws SenMLException {
+        byte[] json = encoder.toSenML(givenDeviceObjectInstance());
+        SenMLPack pack = decoder.fromSenML(json);
+        SenMLTestUtil.assertSenMLPackEquals(givenDeviceObjectInstance(), pack);
+    }
+
+    @Test
+    public void deserialize_device_object() throws SenMLException {
         String dataString = givenSenMLJsonExample();
         SenMLPack pack = decoder.fromSenML(dataString.getBytes());
-        String outString = new String(encoder.toSenML(pack));
-        Assert.assertEquals(dataString.trim(), outString);
+
+        SenMLTestUtil.assertSenMLPackEquals(givenDeviceObjectInstance(), pack);
+    }
+
+    @Test
+    public void serialize_device_object() throws SenMLException {
+        // we skip this test with minimal-json because it encode number using E notation.
+        if (encoder instanceof SenMLJsonMinimalEncoderDecoder)
+            return;
+
+        SenMLPack pack = givenDeviceObjectInstance();
+        byte[] json = encoder.toSenML(pack);
+        assertEquals(givenSenMLJsonExample(), new String(json));
     }
 
 }
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLTestUtil.java b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLTestUtil.java
new file mode 100644
index 00000000..f6cc0bc5
--- /dev/null
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/senml/cbor/SenMLTestUtil.java
@@ -0,0 +1,102 @@
+/*******************************************************************************
+ * 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.senml.cbor;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Objects;
+
+import org.eclipse.leshan.senml.SenMLPack;
+import org.eclipse.leshan.senml.SenMLRecord;
+import org.junit.Assert;
+
+public class SenMLTestUtil {
+
+    public static void assertSenMLPackEquals(SenMLPack expected, SenMLPack actual) {
+        if (expected.getRecords().size() != actual.getRecords().size()) {
+            Assert.fail("Pack not equals : number of records differ");
+        }
+
+        for (int i = 0; i < expected.getRecords().size(); i++) {
+            SenMLRecord r1 = expected.getRecords().get(i);
+            SenMLRecord r2 = actual.getRecords().get(i);
+            assertSenMLRecordEquals(r1, r2);
+        }
+    }
+
+    private static void assertSenMLRecordEquals(SenMLRecord expected, SenMLRecord actual) {
+        assertFieldEquals("basename", expected.getBaseName(), actual.getBaseName());
+        assertFieldEquals("basetime", expected.getBaseTime(), actual.getBaseTime());
+        assertFieldEquals("name", expected.getName(), actual.getName());
+        assertFieldEquals("time", expected.getTime(), actual.getTime());
+        assertFieldEquals("type", expected.getType(), actual.getType());
+        assertFieldEquals("boolean value", expected.getBooleanValue(), actual.getBooleanValue());
+        assertFieldEquals("objlink value", expected.getObjectLinkValue(), actual.getObjectLinkValue());
+        assertFieldEquals("opaque value", expected.getOpaqueValue(), actual.getOpaqueValue());
+        assertFieldEquals("string value", expected.getStringValue(), actual.getStringValue());
+
+        if (!equals(expected.getFloatValue(), actual.getFloatValue())) {
+            Assert.fail(String.format("Records not equals :float value differ expected %s, actual %s",
+                    expected.getFloatValue(), actual.getFloatValue()));
+        }
+    }
+
+    private static void assertFieldEquals(String fieldName, Object expected, Object actual) {
+        if (!Objects.deepEquals(expected, actual)) {
+            Assert.fail(String.format("Records not equals : %s differ expected %s, actual %s", fieldName, expected,
+                    actual));
+        }
+    }
+
+    private static boolean equals(Number x, Number y) {
+        if (x == null && y == null)
+            return true;
+        if (x != null && y == null || x == null && y != null)
+            return false;
+        return compare(x, y) == 0;
+    }
+
+    private static int compare(Number x, Number y) {
+        if (isSpecial(x) || isSpecial(y))
+            return Double.compare(x.doubleValue(), y.doubleValue());
+        else
+            return toBigDecimal(x).compareTo(toBigDecimal(y));
+    }
+
+    private static boolean isSpecial(Number x) {
+        boolean specialDouble = x instanceof Double && (Double.isNaN((Double) x) || Double.isInfinite((Double) x));
+        boolean specialFloat = x instanceof Float && (Float.isNaN((Float) x) || Float.isInfinite((Float) x));
+        return specialDouble || specialFloat;
+    }
+
+    private static BigDecimal toBigDecimal(Number number) {
+        if (number instanceof BigDecimal)
+            return (BigDecimal) number;
+        if (number instanceof BigInteger)
+            return new BigDecimal((BigInteger) number);
+        if (number instanceof Byte || number instanceof Short || number instanceof Integer || number instanceof Long)
+            return new BigDecimal(number.longValue());
+        if (number instanceof Float || number instanceof Double)
+            return new BigDecimal(number.doubleValue());
+
+        try {
+            return new BigDecimal(number.toString());
+        } catch (final NumberFormatException e) {
+            throw new RuntimeException("The given number (\"" + number + "\" of class " + number.getClass().getName()
+                    + ") does not have a parsable string representation", e);
+        }
+    }
+}