Repository: eclipse/leshan
Pull request #851 (2020-06-11 14:34:47)

Add Validator for DDF File and Object Model
This PR was triggered by #835.

The PR provides a DDF file validator and an Object Model Validator.
The DDF file validator validate the file using the LWM2M.xsd schema.
And Object Model Validator do more advanced validation like validate URN or version format.

ObjectLoader can now load a model using validation or not.
Validation is not free and so it could make sense to not validate a trusted model like the one embedded in a jar.




Patch:
diff --git a/leshan-client-demo/pom.xml b/leshan-client-demo/pom.xml
index 51f50b16..7e385f87 100644
--- a/leshan-client-demo/pom.xml
+++ b/leshan-client-demo/pom.xml
@@ -42,6 +42,13 @@ Contributors:
             <artifactId>slf4j-simple</artifactId>
             <scope>runtime</scope>
         </dependency>
+
+        <!-- test dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
index 927f8392..50d993b0 100644
--- a/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
+++ b/leshan-client-demo/src/main/java/org/eclipse/leshan/client/demo/LeshanClientDemo.java
@@ -83,7 +83,7 @@ public class LeshanClientDemo {
 
     // /!\ This field is a COPY of org.eclipse.leshan.server.demo.LeshanServerDemo.modelPaths /!\
     // TODO create a leshan-demo project ?
-    private final static String[] modelPaths = new String[] { "10241.xml", "10242.xml", "10243.xml", "10244.xml",
+    public final static String[] modelPaths = new String[] { "10241.xml", "10242.xml", "10243.xml", "10244.xml",
                             "10245.xml", "10246.xml", "10247.xml", "10248.xml", "10249.xml", "10250.xml", "10251.xml",
                             "10252.xml", "10253.xml", "10254.xml", "10255.xml", "10256.xml", "10257.xml", "10258.xml",
                             "10259.xml", "10260-2_0.xml", "10262.xml", "10263.xml", "10264.xml", "10265.xml",
@@ -451,7 +451,7 @@ public class LeshanClientDemo {
         List<ObjectModel> models = ObjectLoader.loadDefault();
         models.addAll(ObjectLoader.loadDdfResources("/models", modelPaths));
         if (modelsFolderPath != null) {
-            models.addAll(ObjectLoader.loadObjectsFromDir(new File(modelsFolderPath)));
+            models.addAll(ObjectLoader.loadObjectsFromDir(new File(modelsFolderPath), true));
         }
 
         // Initialize object list
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileParser.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileParser.java
index b3791de9..26f38c80 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileParser.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileParser.java
@@ -17,6 +17,7 @@ package org.eclipse.leshan.core.model;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -24,15 +25,18 @@ import java.util.List;
 
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
 
 import org.eclipse.leshan.core.model.ResourceModel.Operations;
 import org.eclipse.leshan.core.model.ResourceModel.Type;
 import org.eclipse.leshan.core.util.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.w3c.dom.DOMException;
 import org.w3c.dom.Document;
 import org.w3c.dom.Node;
 import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
 
 /**
  * A parser for Object DDF files.
@@ -42,11 +46,28 @@ public class DDFFileParser {
     private static final Logger LOG = LoggerFactory.getLogger(DDFFileParser.class);
 
     private final DocumentBuilderFactory factory;
+    private final DDFFileValidator ddfValidator;
 
     public DDFFileParser() {
+        this(null);
+    }
+
+    /**
+     * Build a DDFFileParser with a given {@link DDFFileValidator}.
+     * 
+     * @param ddfValidator a {@link DDFFileValidator} or {@code null} if no validation required.
+     * @since 1.1
+     */
+    public DDFFileParser(DDFFileValidator ddfValidator) {
         factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        this.ddfValidator = ddfValidator;
     }
 
+    /**
+     * @deprecated use {@link #parseEx(File)}
+     */
+    @Deprecated
     public List<ObjectModel> parse(File ddfFile) {
         try (InputStream input = new FileInputStream(ddfFile)) {
             return parse(input, ddfFile.getName());
@@ -56,19 +77,30 @@ public class DDFFileParser {
         return Collections.emptyList();
     }
 
+    /**
+     * @deprecated use {@link #parseEx(InputStream, String)}
+     */
+    @Deprecated
     public List<ObjectModel> parse(InputStream inputStream, String streamName) {
         streamName = streamName == null ? "" : streamName;
 
         LOG.debug("Parsing DDF file {}", streamName);
 
         try {
+            // Parse XML file
             DocumentBuilder builder = factory.newDocumentBuilder();
             Document document = builder.parse(inputStream);
 
+            // Validate XML against Schema
+            if (ddfValidator != null) {
+                ddfValidator.validate(document);
+            }
+
+            // Build list of ObjectModel
             ArrayList<ObjectModel> objects = new ArrayList<>();
             NodeList nodeList = document.getDocumentElement().getElementsByTagName("Object");
             for (int i = 0; i < nodeList.getLength(); i++) {
-                objects.add(parseObject(nodeList.item(i), streamName));
+                objects.add(parseObject(nodeList.item(i), streamName, false));
             }
             return objects;
         } catch (Exception e) {
@@ -77,13 +109,66 @@ public class DDFFileParser {
         return Collections.emptyList();
     }
 
-    private ObjectModel parseObject(Node object, String streamName) {
+    /**
+     * Parse a DDF file.
+     * 
+     * @throws InvalidDDFFileException if DDF file is not a valid.
+     * @throws IOException see {@link FileInputStream#FileInputStream(File) or DocumentBuilder#parse(InputStream)
+     * 
+     * @since 1.1
+     */
+    public List<ObjectModel> parseEx(File ddfFile) throws InvalidDDFFileException, IOException {
+        try (InputStream input = new FileInputStream(ddfFile)) {
+            return parseEx(input, ddfFile.getName());
+        }
+    }
+
+    /**
+     * Parse a DDF file from an inputstream.
+     * 
+     * @throws InvalidDDFFileException if DDF file is not a valid.
+     * @throws IOException see {@link FileInputStream#FileInputStream(File) or DocumentBuilder#parse(InputStream)
+     * 
+     * @since 1.1
+     */
+    public List<ObjectModel> parseEx(InputStream inputStream, String streamName)
+            throws InvalidDDFFileException, IOException {
+        streamName = streamName == null ? "" : streamName;
+
+        LOG.debug("Parsing DDF file {}", streamName);
 
-        // TODO All the validation here should be replaced by a real Validator
+        // Parse XML file
+        try {
+            DocumentBuilder builder = factory.newDocumentBuilder();
+            Document document = builder.parse(inputStream);
+            // Validate XML against Schema
+            if (ddfValidator != null) {
+                ddfValidator.validate(document);
+            }
+
+            // Build list of ObjectModel
+            ArrayList<ObjectModel> objects = new ArrayList<>();
+            NodeList nodeList = document.getDocumentElement().getElementsByTagName("Object");
+            for (int i = 0; i < nodeList.getLength(); i++) {
+                objects.add(parseObject(nodeList.item(i), streamName, ddfValidator != null));
+            }
+            return objects;
+        } catch (InvalidDDFFileException | SAXException e) {
+            throw new InvalidDDFFileException(e, "Invalid DDF file %s", streamName);
+        } catch (ParserConfigurationException e) {
+            throw new IllegalStateException("Unable to create Document Builder", e);
+        }
+    }
+
+    private ObjectModel parseObject(Node object, String streamName, boolean raiseException)
+            throws InvalidDDFFileException {
 
         Node objectType = object.getAttributes().getNamedItem("ObjectType");
         if (objectType == null || !"MODefinition".equals(objectType.getTextContent())) {
-            LOG.warn("Object element in {} MUST have a ObjectType attribute equals to 'MODefinition'", streamName);
+            // TODO change for v2.0 : we keep it for backward compatibility
+            // test must be done only if validation is needed
+            handleError(raiseException,
+                    "Object element in %s MUST have a ObjectType attribute equals to 'MODefinition'.", streamName);
         }
 
         Integer id = null;
@@ -93,6 +178,9 @@ public class DDFFileParser {
         boolean multiple = false;
         boolean mandatory = false;
         List<ResourceModel> resources = new ArrayList<>();
+        String urn = null;
+        String description2 = null;
+        String lwm2mVersion = null;
 
         for (int i = 0; i < object.getChildNodes().getLength(); i++) {
             Node field = object.getChildNodes().item(i);
@@ -119,8 +207,10 @@ public class DDFFileParser {
                 } else if ("Single".equals(field.getTextContent())) {
                     multiple = false;
                 } else {
-                    LOG.warn(
-                            "Invalid Value for [MultipleInstances] field in {}. It MUST be 'Multiple' or 'Single' but it was {} : We will consider it as a 'Single' value.",
+                    // TODO remove for v2.0 : we keep it for backward compatibility
+                    // test will be done in DDFFileValidator or ObjectModelValidatorif validation if needed
+                    handleError(raiseException,
+                            "Invalid Value for [MultipleInstances] field in %s. It MUST be 'Multiple' or 'Single' but it was %s.",
                             field.getTextContent(), streamName);
                     multiple = false;
                 }
@@ -131,8 +221,10 @@ public class DDFFileParser {
                 } else if ("Optional".equals(field.getTextContent())) {
                     mandatory = false;
                 } else {
-                    LOG.warn(
-                            "Invalid Value for [Mandatory] field in {}. It MUST be 'Mandatory' or 'Optional' but it was {} :s We will consider it as a 'Optional' value.",
+                    // TODO remove for v2.0 : we keep it for backward compatibility
+                    // test will be done in DDFFileValidator or ObjectModelValidator if validation if needed
+                    handleError(raiseException,
+                            "Invalid Value for [Mandatory] field in %s. It MUST be 'Mandatory' or 'Optional' but it was %s.",
                             field.getTextContent(), streamName);
                     mandatory = false;
                 }
@@ -144,29 +236,40 @@ public class DDFFileParser {
                         continue;
 
                     if (item.getNodeName().equals("Item")) {
-                        resources.add(this.parseResource(item, streamName));
+                        resources.add(this.parseResource(item, streamName, raiseException));
                     } else {
-                        LOG.warn("Unexpected resources element [{}] in {} : it will be ignored.", item.getNodeName(),
-                                streamName);
+                        // TODO remove for v2.0 : we keep it for backward compatibility
+                        // test will be done in DDFFileValidator if validation if needed
+                        handleError(raiseException, "Unexpected resources element [%s] in %s : it will be ignored.",
+                                item.getNodeName(), streamName);
                     }
                 }
                 break;
             case "ObjectURN":
+                urn = field.getTextContent();
+                break;
             case "LWM2MVersion":
+                lwm2mVersion = field.getTextContent();
+                break;
             case "Description2":
-                // TODO it should be supported in Leshan v1.1 or later.
+                description2 = field.getTextContent();
                 break;
             default:
-                LOG.warn("Unexpected object element [{}] in {} : it will be ignored.", field.getNodeName(), streamName);
+                // TODO remove for v2.0 : we keep it for backward compatibility
+                // test will be done in DDFFileValidator if validation if needed
+                handleError(raiseException, "Unexpected object element [%s] in %s : it will be ignored.",
+                        field.getNodeName(), streamName);
                 break;
             }
         }
 
-        return new ObjectModel(id, name, description, version, multiple, mandatory, resources);
+        return new ObjectModel(id, name, description, version, multiple, mandatory, resources, urn, lwm2mVersion,
+                description2);
 
     }
 
-    private ResourceModel parseResource(Node item, String streamName) {
+    private ResourceModel parseResource(Node item, String streamName, boolean raiseException)
+            throws DOMException, InvalidDDFFileException {
 
         Integer id = Integer.valueOf(item.getAttributes().getNamedItem("ID").getTextContent());
         String name = null;
@@ -199,8 +302,10 @@ public class DDFFileParser {
                 } else if ("Single".equals(field.getTextContent())) {
                     multiple = false;
                 } else {
-                    LOG.warn(
-                            "Invalid Value for [MultipleInstances] field in {}. It MUST be 'Multiple' or 'Single' but it was {} : We will consider it as a 'Single' value.",
+                    // TODO remove for v2.0 : we keep it for backward compatibility
+                    // test will be done in DDFFileValidator or ObjectModelValidatorif validation if needed
+                    handleError(raiseException,
+                            "Invalid Value for [MultipleInstances] field in %s. It MUST be 'Multiple' or 'Single' but it was %s : We will consider it as a 'Single' value.",
                             field.getTextContent(), streamName);
                     multiple = false;
                 }
@@ -211,8 +316,10 @@ public class DDFFileParser {
                 } else if ("Optional".equals(field.getTextContent())) {
                     mandatory = false;
                 } else {
-                    LOG.warn(
-                            "Invalid Value for [Mandatory] field in {}. It MUST be 'Mandatory' or 'Optional' but it was {} :s We will consider it as a 'Optional' value.",
+                    // TODO remove for v2.0 : we keep it for backward compatibility
+                    // test will be done in DDFFileValidator or ObjectModelValidatorif validation if needed
+                    handleError(raiseException,
+                            "Invalid Value for [Mandatory] field in %s. It MUST be 'Mandatory' or 'Optional' but it was %s :s We will consider it as a 'Optional' value.",
                             field.getTextContent(), streamName);
                     mandatory = false;
                 }
@@ -244,7 +351,10 @@ public class DDFFileParser {
                     type = null;
                     break;
                 default:
-                    LOG.warn("Unexpected type value [{}] in {} : no type assigned", field.getTextContent(), streamName);
+                    // TODO remove for v2.0 : we keep it for backward compatibility
+                    // test will be done in DDFFileValidator validation if needed
+                    handleError(raiseException, "Unexpected type value [%s] in %s : no type assigned",
+                            field.getTextContent(), streamName);
                     break;
                 }
                 break;
@@ -258,22 +368,37 @@ public class DDFFileParser {
                 description = field.getTextContent();
                 break;
             default:
-                LOG.warn("Unexpected resource element [{} {}] in {} : it will be ignored.", field.getNodeName(),
-                        field.getNodeType(), streamName);
+                // TODO remove for v2.0 : we keep it for backward compatibility
+                // test will be done in DDFFileValidator validation if needed
+                handleError(raiseException, "Unexpected resource element [%s %s] in %s : it will be ignored.",
+                        field.getNodeName(), field.getNodeType(), streamName);
                 break;
             }
         }
 
-        if (operations.isExecutable() && type != null) {
-            LOG.warn("Model for Resource {}({}) in {} is invalid : an executable resource MUST NOT have a type({})",
-                    name, id, streamName, type);
-        } else if (!operations.isExecutable() && type == null) {
-            LOG.warn(
-                    "Model for Resource {}({}) in {} is invalid : a none executable resource MUST have a type. We will consider it as String.",
-                    name, id, streamName, type);
-            type = Type.STRING;
+        // TODO remove for v2.0 : we keep it for backward compatibility
+        // test will be done in ObjetModelValidator if validation if needed
+        if (!raiseException) {
+            if (operations.isExecutable() && type != null) {
+                LOG.warn("Model for Resource {}({}) in {} is invalid : an executable resource MUST NOT have a type({})",
+                        name, id, streamName, type);
+            } else if (!operations.isExecutable() && type == null) {
+                LOG.warn(
+                        "Model for Resource {}({}) in {} is invalid : a none executable resource MUST have a type. We will consider it as String.",
+                        name, id, streamName, type);
+                type = Type.STRING;
+            }
         }
 
         return new ResourceModel(id, name, operations, multiple, mandatory, type, rangeEnumeration, units, description);
     }
+
+    private void handleError(boolean raiseException, String Message, Object... args) throws InvalidDDFFileException {
+        String msg = String.format(Message, args);
+        if (raiseException) {
+            throw new InvalidDDFFileException(msg);
+        } else {
+            LOG.warn(msg);
+        }
+    }
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileValidator.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileValidator.java
new file mode 100644
index 00000000..cf0a7abc
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/DDFFileValidator.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * 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.model;
+
+import org.w3c.dom.Node;
+
+/**
+ * A DDF File Validator.
+ * <p>
+ * Validate a DDF File against a LWM2M.xsd schema.
+ * 
+ * @since 1.1
+ */
+public interface DDFFileValidator {
+
+    /**
+     * Validate a DOM node (could be DOM Document) against the LWM2M.xsd Schema.
+     * 
+     * @param xmlToValidate DOM node to validate
+     * @throws InvalidDDFFileException if ddf file is invalid.
+     */
+    public void validate(Node xmlToValidate) throws InvalidDDFFileException;
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/DefaultDDFFileValidator.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/DefaultDDFFileValidator.java
new file mode 100644
index 00000000..c03156dd
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/DefaultDDFFileValidator.java
@@ -0,0 +1,75 @@
+/*******************************************************************************
+ * 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.model;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+import javax.xml.XMLConstants;
+import javax.xml.transform.Source;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.Validator;
+
+import org.w3c.dom.Node;
+import org.xml.sax.SAXException;
+
+/**
+ * A DDF File Validator.
+ * <p>
+ * Validate a DDF File against the embedded LWM2M.xsd schema.
+ * 
+ * @since 1.1
+ */
+
+public class DefaultDDFFileValidator implements DDFFileValidator {
+    private static String LWM2M_V1_SCHEMA_PATH = "/schemas/LWM2M.xsd";
+
+    @Override
+    public void validate(Node xmlToValidate) throws InvalidDDFFileException {
+        try {
+            validate(new DOMSource(xmlToValidate));
+        } catch (SAXException | IOException e) {
+            throw new InvalidDDFFileException(e);
+        }
+    }
+
+    /**
+     * Validate a XML {@link Source} against the embedded LWM2M.xsd Schema.
+     * 
+     * @param xmlToValidate an XML source to validate
+     * @throws SAXException see {@link Validator#validate(Source)}
+     * @throws IOException see {@link Validator#validate(Source)}
+     */
+    public void validate(Source xmlToValidate) throws SAXException, IOException {
+        Validator validator = getEmbeddedLwM2mSchema().newValidator();
+        validator.validate(xmlToValidate);
+    }
+
+    /**
+     * Get the Embedded the LWM2M.xsd Schema.
+     * 
+     * @throws SAXException see {@link SchemaFactory#newSchema(Source))}
+     */
+    protected Schema getEmbeddedLwM2mSchema() throws SAXException {
+        InputStream inputStream = DDFFileValidator.class.getResourceAsStream(LWM2M_V1_SCHEMA_PATH);
+        Source source = new StreamSource(inputStream);
+        SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+        return schemaFactory.newSchema(source);
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/DefaultObjectModelValidator.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/DefaultObjectModelValidator.java
new file mode 100644
index 00000000..a8720001
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/DefaultObjectModelValidator.java
@@ -0,0 +1,163 @@
+/*******************************************************************************
+ * 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.model;
+
+import java.util.List;
+
+public class DefaultObjectModelValidator implements ObjectModelValidator {
+
+    @Override
+    public void validate(List<ObjectModel> models, String modelName) throws InvalidModelException {
+        for (ObjectModel model : models) {
+            validate(model, modelName);
+        }
+    }
+
+    /**
+     * Validate a {@link ObjectModel}.
+     * 
+     * @param object the {@link ObjectModel} to validate
+     * @param modelName a hint about where the object come from to make debug easier. e.g a filename in model was store
+     *        in a file.
+     * @throws InvalidModelException is raised when {@link ObjectModel} is Invalid
+     */
+    public void validate(ObjectModel object, String modelName) throws InvalidModelException {
+        if (object.name == null || object.name.isEmpty()) {
+            throw new InvalidModelException(
+                    "Model for Object (%d) in %s is invalid : Object name MUST NOT be null or empty", object.id,
+                    modelName);
+        }
+        if (!(0 <= object.id && object.id <= 42768)) {
+            throw new InvalidModelException(
+                    "Model for Object (%d) in %s is invalid : Object id MUST be between 0 and 42768", object.id,
+                    modelName);
+        }
+        if (1024 <= object.id && object.id <= 2047) {
+            throw new InvalidModelException(
+                    "Model for Object (%d) in %s is invalid : Object id is in reserved range (1024-2047)", object.id,
+                    modelName);
+        }
+        validateVersion(object.version, object, modelName);
+        validateURN(object.urn, object, modelName);
+        if (object.resources == null || object.resources.isEmpty()) {
+            throw new InvalidModelException(
+                    "Model for Object %s(%d) in %s is invalid : Resource lists MUST NOT be null or empty", object.name,
+                    object.id, modelName);
+        }
+        for (ResourceModel resource : object.resources.values()) {
+            validate(resource, modelName);
+        }
+    }
+
+    /**
+     * Validate an {@link ResourceModel}
+     * 
+     * @param resource the {@link ResourceModel} to validate
+     * @param modelName a hint about where the resource come from to make debug easier. e.g a filename in model was
+     *        store in a file.
+     * @throws InvalidModelException is raised when {@link ResourceModel} is Invalid
+     */
+    public void validate(ResourceModel resource, String modelName) throws InvalidModelException {
+        if (resource.name == null || resource.name.isEmpty()) {
+            throw new InvalidModelException(
+                    "Model for Resource (%d) in %s is invalid : resource name MUST NOT be null or empty", resource.id,
+                    modelName);
+        }
+        if (!(0 <= resource.id && resource.id <= 32768)) {
+            throw new InvalidModelException(
+                    "Model for Resource (%d) in %s is invalid : Resource id MUST be between 0 and 32768", resource.id,
+                    modelName);
+        }
+        // TODO in 2.0 : type must be NONE
+        if (resource.operations.isExecutable() && resource.type != null) {
+            throw new InvalidModelException(
+                    "Model for Resource %s(%d) in %s is invalid : an executable resource MUST NOT have a type(%s)",
+                    resource.name, resource.id, modelName, resource.type);
+        } else if (!resource.operations.isExecutable() && resource.type == null) {
+            throw new InvalidModelException(
+                    "Model for Resource %s(%d) in %s is invalid : a none executable resource MUST have a type.",
+                    resource.name, resource.id, modelName, resource.type);
+        }
+    }
+
+    protected void validateURN(String urn, ObjectModel object, String modelName) throws InvalidModelException {
+        if (!urn.startsWith("urn:oma:lwm2m:")) {
+            throw new InvalidModelException(
+                    "Model for Object %s(%d) in %s is invalid : URN (%s) MUST start with urn:oma:lwm2m.", object.name,
+                    object.id, modelName, urn);
+        }
+        String[] urnParts = urn.split(":");
+        if (urnParts.length != 5 && urnParts.length != 6) {
+            throw new InvalidModelException(
+                    "Model for Object %s(%d) in %s is invalid : URN (%s) MUST be composed of 5 or 6 parts.",
+                    object.name, object.id, modelName, urn);
+        }
+        String objectId = urnParts[4];
+        if (!objectId.equals(Integer.toString(object.id))) {
+            throw new InvalidModelException(
+                    "Model for Object %s(%d) in %s is invalid : URN (%s) object id part MUST be equals to object id",
+                    object.name, object.id, modelName, urn);
+        }
+        String kind = urnParts[3];
+        String expectedKind = URN.getUrnKind(object.id);
+        if (!expectedKind.equals(kind)) {
+            throw new InvalidModelException(
+                    "Model for Object %s(%d) in %s is invalid : URN (%s) kind MUST be %s instead of %s", object.name,
+                    object.id, modelName, urn, expectedKind, kind);
+        }
+
+        if (urnParts.length == 6) {
+            String version = urnParts[5];
+            String expectedVersion = object.getVersion();
+            if (!expectedVersion.equals(version)) {
+                throw new InvalidModelException(
+                        "Model for Object %s(%d) in %s is invalid : URN (%s) version MUST be equals to object version (%s)",
+                        object.name, object.id, modelName, urn, expectedVersion);
+            }
+        } else {
+            if (!ObjectModel.DEFAULT_VERSION.equals(object.getVersion())) {
+                throw new InvalidModelException(
+                        "Model for Object %s(%d) in %s is invalid : URN (%s) version MUST be present as object version is not %s",
+                        object.name, object.id, modelName, urn, ObjectModel.DEFAULT_VERSION);
+            }
+        }
+    }
+
+    protected void validateVersion(String version, ObjectModel object, String modelName) throws InvalidModelException {
+        if (version == null || version.isEmpty())
+            return;
+        String[] versionPart = version.split("\\.");
+        if (versionPart.length != 2) {
+            throw new InvalidModelException(
+                    "Model for Object %s(%d) in %s is invalid : Version (%s) MUST be composed of 2 parts", object.name,
+                    object.id, modelName, version);
+        }
+        for (int i = 0; i < 2; i++) {
+            try {
+                int parsedInt = Integer.parseInt(versionPart[i]);
+                if (parsedInt < 0) {
+                    throw new InvalidModelException(
+                            "Model for Object %s(%d) in %s is invalid : Version (%s) part %d (%s) is not a valid integer",
+                            object.name, object.id, modelName, version, i + 1, versionPart[i]);
+                }
+            } catch (Exception e) {
+                throw new InvalidModelException(
+                        "Model for Object %s(%d) in %s is invalid : Version (%s) part %d (%s) is not a valid integer",
+                        object.name, object.id, modelName, version, i + 1, versionPart);
+            }
+        }
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/InvalidDDFFileException.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/InvalidDDFFileException.java
new file mode 100644
index 00000000..84fb1796
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/InvalidDDFFileException.java
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * 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.model;
+
+/**
+ * Raised by {@link DDFFileValidator} if a DDF file is invalid
+ * 
+ * @since 1.1
+ */
+public class InvalidDDFFileException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public InvalidDDFFileException() {
+    }
+
+    public InvalidDDFFileException(String m) {
+        super(m);
+    }
+
+    public InvalidDDFFileException(String m, Object... args) {
+        super(String.format(m, args));
+    }
+
+    public InvalidDDFFileException(Throwable e) {
+        super(e);
+    }
+
+    public InvalidDDFFileException(String m, Throwable e) {
+        super(m, e);
+    }
+
+    public InvalidDDFFileException(Throwable e, String m, Object... args) {
+        super(String.format(m, args), e);
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/InvalidModelException.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/InvalidModelException.java
new file mode 100644
index 00000000..7de99d83
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/InvalidModelException.java
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * 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.model;
+
+/**
+ * An exception raised when {@link ObjectModelValidator} detect an error.
+ * 
+ * @since 1.1
+ */
+public class InvalidModelException extends Exception {
+
+    private static final long serialVersionUID = 1L;
+
+    public InvalidModelException(String m) {
+        super(m);
+    }
+
+    public InvalidModelException(String m, Object... args) {
+        super(String.format(m, args));
+    }
+
+    public InvalidModelException(Throwable e) {
+        super(e);
+    }
+
+    public InvalidModelException(String m, Throwable e) {
+        super(m, e);
+    }
+
+    public InvalidModelException(Throwable e, String m, Object... args) {
+        super(String.format(m, args), e);
+    }
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectLoader.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectLoader.java
index 624beb8e..88d97d67 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectLoader.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectLoader.java
@@ -17,12 +17,14 @@ package org.eclipse.leshan.core.model;
 
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.Reader;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 import org.eclipse.leshan.core.util.StringUtils;
@@ -33,7 +35,7 @@ public class ObjectLoader {
 
     private static final Logger LOG = LoggerFactory.getLogger(ObjectLoader.class);
 
-    private static final String[] ddfpaths = new String[] { "LWM2M_Security-v1_0.xml", "LWM2M_Server-v1_0.xml",
+    static final String[] ddfpaths = new String[] { "LWM2M_Security-v1_0.xml", "LWM2M_Server-v1_0.xml",
                             "LWM2M_Access_Control-v1_0_3.xml", "LWM2M_Device-v1_0_3.xml",
                             "LWM2M_Connectivity_Monitoring-v1_0_2.xml", "LWM2M_Firmware_Update-v1_0_3.xml",
                             "LWM2M_Location-v1_0_2.xml", "LWM2M_Connectivity_Statistics-v1_0_4.xml" };
@@ -55,52 +57,169 @@ public class ObjectLoader {
      * Load object definitions from DDF or JSON files.
      * 
      * @param modelDir the directory containing the object definition files.
+     * 
+     * @deprecated use {@link #loadObjectsFromDir(File)} instead
      */
+    @Deprecated
     public static List<ObjectModel> load(File modelDir) {
         return loadObjectsFromDir(modelDir);
     }
 
     /**
      * Load object definition from DDF file.
+     * <p>
+     * Models are not validate if you want to ensure that model are valid use
+     * {@link #loadDdfFile(InputStream, String, boolean)} or
+     * {@link #loadDdfFile(InputStream, String, DDFFileValidator, ObjectModelValidator)}
      * 
      * @param input An inputStream to a DDF file.
      * @param streamName A name for the stream used for logging only
      */
     public static List<ObjectModel> loadDdfFile(InputStream input, String streamName) {
-        DDFFileParser ddfFileParser = new DDFFileParser();
-        return ddfFileParser.parse(input, streamName);
+        try {
+            DDFFileParser ddfFileParser = new DDFFileParser();
+            return ddfFileParser.parseEx(input, streamName);
+        } catch (InvalidDDFFileException | IOException e) {
+            // TODO change for 2.0 : we should raise an IOException
+            LOG.warn("Unable to parse ddf file {}", streamName, e);
+        }
+        return Collections.emptyList();
     }
 
     /**
-     * Load object definition from DDF resources following rules of {@link Class#getResourceAsStream(String)}.
+     * Load object definition from DDF file.
+     * 
+     * @param input An inputStream to a DDF file.
+     * @param streamName A name for the stream used for logging only
+     * @param validate true if you want model validation. Validation is not free and it could make sense to not validate
+     *        model if you already trust it.
+     * 
+     * @throws InvalidDDFFileException if DDF file is invalid
+     * @throws InvalidModelException if model is invalid
+     * @throws IOException
+     * 
+     * @since 1.1
+     */
+    public static List<ObjectModel> loadDdfFile(InputStream input, String streamName, boolean validate)
+            throws InvalidModelException, InvalidDDFFileException, IOException {
+        return loadDdfFile(input, streamName, validate ? new DefaultDDFFileValidator() : null,
+                validate ? new DefaultObjectModelValidator() : null);
+    }
+
+    /**
+     * Load object definition from DDF file.
+     * 
+     * @param input An inputStream to a DDF file.
+     * @param streamName A name for the stream used for logging and validation
+     * @param ddfValidator a validator used to validate DDF files, see {@link DefaultDDFFileValidator}. If {@code null}
+     *        then there will be no validation.
+     * @param modelValidator an Object model validator to ensure model is valid, see
+     *        {@link DefaultObjectModelValidator}. If {@code null} then there will be no validation.
+     * 
+     * @throws InvalidDDFFileException if DDF file is invalid
+     * @throws InvalidModelException if model is invalid
+     * @throws IOException
      * 
+     * @since 1.1
+     */
+    public static List<ObjectModel> loadDdfFile(InputStream input, String streamName, DDFFileValidator ddfValidator,
+            ObjectModelValidator modelValidator) throws InvalidModelException, InvalidDDFFileException, IOException {
+        DDFFileParser ddfFileParser = new DDFFileParser(ddfValidator);
+        List<ObjectModel> models = ddfFileParser.parseEx(input, streamName);
+        if (modelValidator != null) {
+            modelValidator.validate(models, streamName);
+        }
+        return models;
+    }
+
+    /**
+     * Load object definition from DDF resources following rules of {@link Class#getResourceAsStream(String)}.
+     * <p>
      * It should be used to load DDF embedded with your application bundle (e.g. jar, war, ...)
+     * <p>
+     * Models are not validate if you want to ensure that model are valid use
+     * {@link #loadDdfFile(InputStream, String, boolean)} or
+     * {@link #loadDdfFile(InputStream, String, DDFFileValidator, ObjectModelValidator)}
      * 
      * @param path directory path to the DDF files
      * @param filenames names of all the DDF files
      */
     public static List<ObjectModel> loadDdfResources(String path, String[] filenames) {
+        try {
+            return loadDdfResources(path, filenames, false);
+        } catch (IOException | InvalidModelException | InvalidDDFFileException e) {
+            throw new IllegalStateException("Unable to load model", e);
+        }
+
+    }
+
+    /**
+     * Load object definition from DDF resources following rules of {@link Class#getResourceAsStream(String)}.
+     * <p>
+     * It should be used to load DDF embedded with your application bundle (e.g. jar, war, ...)
+     * 
+     * @param path directory path to the DDF files
+     * @param filenames names of all the DDF files
+     * @param validate true if you want model validation. Validation is not free and it could make sense to not validate
+     *        model if you already trust it.
+     * 
+     * @throws InvalidDDFFileException if DDF file is invalid
+     * @throws InvalidModelException if model is invalid
+     * @throws IOException
+     * 
+     * @since 1.1
+     */
+    public static List<ObjectModel> loadDdfResources(String path, String[] filenames, boolean validate)
+            throws IOException, InvalidModelException, InvalidDDFFileException {
+        return loadDdfResources(path, filenames, validate ? new DefaultDDFFileValidator() : null,
+                validate ? new DefaultObjectModelValidator() : null);
+    }
+
+    /**
+     * Load object definition from DDF resources following rules of {@link Class#getResourceAsStream(String)}.
+     * <p>
+     * It should be used to load DDF embedded with your application bundle (e.g. jar, war, ...)
+     * <p>
+     * 
+     * @param path directory path to the DDF files
+     * @param filenames names of all the DDF files
+     * @param ddfValidator a validator used to validate DDF files, see {@link DefaultDDFFileValidator}. If {@code null}
+     *        then there will be no validation.
+     * @param modelValidator an Object model validator to ensure model is valid, see
+     *        {@link DefaultObjectModelValidator}. If {@code null} then there will be no validation.
+     * 
+     * @throws InvalidDDFFileException if DDF file is invalid
+     * @throws InvalidModelException if model is invalid
+     * @throws IOException
+     * 
+     * @since 1.1
+     */
+    public static List<ObjectModel> loadDdfResources(String path, String[] filenames, DDFFileValidator ddfValidator,
+            ObjectModelValidator modelValidator) throws IOException, InvalidModelException, InvalidDDFFileException {
         List<ObjectModel> models = new ArrayList<>();
         for (String filename : filenames) {
             String fullpath = StringUtils.removeEnd(path, "/") + "/" + StringUtils.removeStart(filename, "/");
             InputStream input = ObjectLoader.class.getResourceAsStream(fullpath);
             if (input != null) {
                 try (Reader reader = new InputStreamReader(input)) {
-                    models.addAll(loadDdfFile(input, fullpath));
-                } catch (IOException e) {
-                    throw new IllegalStateException(String.format("Unable to load model %s", fullpath), e);
+                    models.addAll(loadDdfFile(input, fullpath, ddfValidator, modelValidator));
                 }
             } else {
-                throw new IllegalStateException(String.format("Unable to load model %s", fullpath));
+                throw new FileNotFoundException(String.format("%s not found", fullpath));
             }
+
         }
         return models;
     }
 
     /**
      * Load object definition from DDF resources following rules of {@link Class#getResourceAsStream(String)}.
-     * 
+     * <p>
      * It should be used to load DDF embedded with your application bundle (e.g. jar, war, ...)
+     * <p>
+     * Models are not validate if you want to ensure that model are valid use
+     * {@link #loadDdfFile(InputStream, String, boolean)} or
+     * {@link #loadDdfFile(InputStream, String, DDFFileValidator, ObjectModelValidator)}
      * 
      * @param paths An array of paths to DDF files.
      */
@@ -121,10 +240,48 @@ public class ObjectLoader {
         return models;
     }
 
-    /*
-     * Load object definitions from files
+    /**
+     * Load object definitions from directory.
+     * <p>
+     * Models are not validate if you want to ensure that model are valid use
+     * {@link #loadDdfFile(InputStream, String, boolean)} or
+     * {@link #loadDdfFile(InputStream, String, DDFFileValidator, ObjectModelValidator)}
+     * 
+     * @param modelsDir the directory containing all the ddf file definition.
      */
     public static List<ObjectModel> loadObjectsFromDir(File modelsDir) {
+        return loadObjectsFromDir(modelsDir, null, null);
+    }
+
+    /**
+     * Load object definitions from directory.
+     * 
+     * Invalid model will be logged and ignored.
+     * 
+     * @param modelsDir the directory containing all the ddf file definition.
+     * @param validate true if you want model validation. Validation is not free and it could make sense to not validate
+     *        model if you already trust it.
+     */
+    public static List<ObjectModel> loadObjectsFromDir(File modelsDir, boolean validate) {
+        return loadObjectsFromDir(modelsDir, validate ? new DefaultDDFFileValidator() : null,
+                validate ? new DefaultObjectModelValidator() : null);
+    }
+
+    /**
+     * Load object definitions from directory.
+     * <p>
+     * Invalid model will be logged and ignored.
+     * 
+     * @param modelsDir the directory containing all the ddf file definition.
+     * @param ddfValidator a validator used to validate DDF files, see {@link DefaultDDFFileValidator}. If {@code null}
+     *        then there will be no validation.
+     * @param modelValidator an Object model validator to ensure model is valid, see
+     *        {@link DefaultObjectModelValidator}. If {@code null} then there will be no validation.
+     * 
+     * @since 1.1
+     */
+    public static List<ObjectModel> loadObjectsFromDir(File modelsDir, DDFFileValidator ddfValidator,
+            ObjectModelValidator modelValidator) {
         List<ObjectModel> models = new ArrayList<>();
 
         // check if the folder is usable
@@ -142,12 +299,11 @@ public class ObjectLoader {
                     // from DDF file
                     LOG.debug("Loading object models from DDF file {}", file.getAbsolutePath());
                     try (FileInputStream input = new FileInputStream(file)) {
-                        models.addAll(loadDdfFile(input, file.getName()));
-                    } catch (IOException e) {
+                        models.addAll(loadDdfFile(input, file.getName(), ddfValidator, modelValidator));
+                    } catch (IOException | InvalidModelException | InvalidDDFFileException e) {
                         LOG.warn(MessageFormat.format("Unable to load object models for {0}", file.getAbsolutePath()),
                                 e);
                     }
-
                 }
             }
         }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModel.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModel.java
index 277563e3..577a9e16 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModel.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModel.java
@@ -21,7 +21,7 @@ import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 
-import org.eclipse.leshan.core.util.Validate;
+import org.eclipse.leshan.core.LwM2m;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -40,12 +40,19 @@ public class ObjectModel {
     private static final int OMA_OBJECT_MIN_ID = 0;
     private static final int OMA_OBJECT_MAX_ID = 1023;
 
+    // TODO in version 2.0 all field should be null-able and ObjectModelValidator should be responsible to validate it.
     public final int id;
     public final String name;
     public final String description;
     public final String version;
     public final boolean multiple;
     public final boolean mandatory;
+    /** @since 1.1 */
+    public final String urn;
+    /** @since 1.1 */
+    public final String lwm2mVersion;
+    /** @since 1.1 */
+    public final String description2;
 
     public final Map<Integer, ResourceModel> resources; // resources by ID
 
@@ -56,7 +63,14 @@ public class ObjectModel {
 
     public ObjectModel(int id, String name, String description, String version, boolean multiple, boolean mandatory,
             Collection<ResourceModel> resources) {
-        Validate.notEmpty(version);
+        this(id, name, description, version, multiple, mandatory, resources, URN.generateURN(id, version), null, "");
+    }
+
+    /**
+     * @since 1.1
+     */
+    public ObjectModel(int id, String name, String description, String version, boolean multiple, boolean mandatory,
+            Collection<ResourceModel> resources, String urn, String lwm2mVersion, String description2) {
 
         this.id = id;
         this.name = name;
@@ -64,6 +78,9 @@ public class ObjectModel {
         this.version = version;
         this.multiple = multiple;
         this.mandatory = mandatory;
+        this.urn = urn;
+        this.lwm2mVersion = lwm2mVersion;
+        this.description2 = description2;
 
         Map<Integer, ResourceModel> resourcesMap = new HashMap<>(resources.size());
         for (ResourceModel resource : resources) {
@@ -91,13 +108,22 @@ public class ObjectModel {
         return version;
     }
 
+    /**
+     * @return the LWM2M version and if the LWM2M version is null or empty return the default value 1.0
+     * @see LwM2m#VERSION
+     * @since 1.1
+     */
+    public String getLwM2mVersion() {
+        if (lwm2mVersion == null || lwm2mVersion.isEmpty()) {
+            return LwM2m.VERSION;
+        }
+        return lwm2mVersion;
+    }
+
     @Override
     public String toString() {
-        StringBuilder builder = new StringBuilder();
-        builder.append("ObjectModel [id=").append(id).append(", name=").append(name).append(", description=")
-                .append(description).append(", version=").append(version).append(", multiple=").append(multiple)
-                .append(", mandatory=").append(mandatory).append(", resources=").append(resources).append("]");
-        return builder.toString();
+        return String.format(
+                "ObjectModel [id=%s, name=%s, description=%s, version=%s, multiple=%s, mandatory=%s, urn=%s, lwm2mVersion=%s, description2=%s, resources=%s]",
+                id, name, description, version, multiple, mandatory, urn, lwm2mVersion, description2, resources);
     }
-
 }
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModelValidator.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModelValidator.java
new file mode 100644
index 00000000..93a5d374
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ObjectModelValidator.java
@@ -0,0 +1,36 @@
+/*******************************************************************************
+ * 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.model;
+
+import java.util.List;
+
+/**
+ * Validate an LWM2M Object or Resource Model.
+ * 
+ * @since 1.1
+ */
+public interface ObjectModelValidator {
+
+    /**
+     * Validate a list of {@link ObjectModel}.
+     * 
+     * @param models the list of {@link ObjectModel} to validate
+     * @param modelName a hint about where the object come from to make debug easier. e.g a filename if model was store
+     *        in a file.
+     * @throws InvalidModelException is raised when an {@link ObjectModel} is Invalid
+     */
+    public void validate(List<ObjectModel> models, String modelName) throws InvalidModelException;
+}
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ResourceModel.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ResourceModel.java
index ccea2f0a..fa9fcd16 100644
--- a/leshan-core/src/main/java/org/eclipse/leshan/core/model/ResourceModel.java
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/ResourceModel.java
@@ -17,6 +17,7 @@ package org.eclipse.leshan.core.model;
 
 /**
  * A resource description
+ * 
  * @see "LWM2M specification D.1 Object Template."
  * @see <a href="http://openmobilealliance.org/tech/profiles/LWM2M.xsd">LWM2M Editor Schema</a>
  */
@@ -38,10 +39,12 @@ public class ResourceModel {
         }
     }
 
+    // TODO in version 2.0 : NONE should be added.
     public enum Type {
         STRING, INTEGER, FLOAT, BOOLEAN, OPAQUE, TIME, OBJLNK
     }
 
+    // TODO in version 2.0 all field should be null-able and ObjectModelValidator should be responsible to validate it.
     public final int id;
     public final String name;
     public final Operations operations;
diff --git a/leshan-core/src/main/java/org/eclipse/leshan/core/model/URN.java b/leshan-core/src/main/java/org/eclipse/leshan/core/model/URN.java
new file mode 100644
index 00000000..9ad679b6
--- /dev/null
+++ b/leshan-core/src/main/java/org/eclipse/leshan/core/model/URN.java
@@ -0,0 +1,63 @@
+/*******************************************************************************
+ * 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.model;
+
+/**
+ * Some utility methods about object URN field of LWM2M Object Model
+ * 
+ * @since 1.1
+ */
+public class URN {
+    public static final String OMA_LABEL = "oma";
+    public static final String EXT_LABEL = "ext";
+    public static final String X_LABEL = "x";
+    public static final String INVALID_LABEL = "invalid";
+    public static final String RESERVED_LABEL = "reserved";
+
+    /**
+     * Generate URN from object Id and Object version.
+     */
+    public static String generateURN(int objectId, String objectVersion) {
+        StringBuilder urn = new StringBuilder("urn:oma:lwm2m");
+        urn.append(":").append(getUrnKind(objectId));
+        urn.append(":").append(objectId);
+        if (objectVersion != null && !objectVersion.isEmpty() && !objectVersion.equals(ObjectModel.DEFAULT_VERSION))
+            urn.append(":").append(objectVersion);
+        return urn.toString();
+    }
+
+    /**
+     * Return URN "kind" from object id.
+     * 
+     * @param objectId
+     * @return {@link #OMA_LABEL}, {@link #EXT_LABEL} or {@link #X_LABEL} for valid object id. {@link #INVALID_LABEL} is
+     *         returned for invalid id and {@link #RESERVED_LABEL} for reserved range.
+     * 
+     * @see <a href="http://www.openmobilealliance.org/wp/OMNA/LwM2M/LwM2MRegistry.html">OMA LightweightM2M (LwM2M)
+     *      Object and Resource Registry </a>
+     */
+    public static String getUrnKind(int objectId) {
+        if (0 <= objectId && objectId <= 1023)
+            return OMA_LABEL;
+        if (1024 <= objectId && objectId <= 2047)
+            return RESERVED_LABEL;
+        if (2048 <= objectId && objectId <= 10240)
+            return EXT_LABEL;
+        if (10241 <= objectId && objectId <= 42768)
+            return X_LABEL;
+        return INVALID_LABEL;
+    }
+}
diff --git a/leshan-core/src/main/resources/schemas/LWM2M.xsd b/leshan-core/src/main/resources/schemas/LWM2M.xsd
new file mode 100644
index 00000000..2f01d5ae
--- /dev/null
+++ b/leshan-core/src/main/resources/schemas/LWM2M.xsd
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+LWM2M XML Schema - LWM2M Editor Schema
+   version - 1.0.1
+   date    - 14 Jan 2017
+
+FILE INFORMATION
+
+  Public Reachable Information
+    Path: http://www.openmobilealliance.org/tech/profiles
+    Name: LWM2M.xsd
+
+NORMATIVE INFORMATION
+
+  Information about this file can be found in the LW M2M online editor
+
+  This is available at http://www.openmobilealliance.org/
+
+  Send comments to technical-comments@mail.openmobilealliance.org
+
+CHANGE HISTORY
+
+21112013 First version
+06082015 Bug Fix to correct mismatch
+18022016 Legal Disclaimer updated from template OMA-TEMPLATE-SUP_XSD_example-20160218-I
+24032016 Changed "xs:unsignedshort" to "xs:unsignedShort"
+25052016 Incorporated OMA-DM-LightweightM2M-2016-0066R01-INP_CR_Update_LWM2M_xsd
+14012017 Introduction of two new elements: LWM2MObject and ObjectVersion
+
+LEGAL DISCLAIMER
+
+  Copyright 2017 Open Mobile Alliance Ltd.  All rights reserved.
+
+  Redistribution and use in source and binary forms, with or without
+  modification, are permitted provided that the following conditions
+  are met:
+
+  1. Redistributions of source code must retain the above copyright
+  notice, this list of conditions and the following disclaimer.
+  2. Redistributions in binary form must reproduce the above copyright
+  notice, this list of conditions and the following disclaimer in the
+  documentation and/or other materials provided with the distribution.
+  3. Neither the name of the copyright holder nor the names of its
+  contributors may be used to endorse or promote products derived
+  from this software without specific prior written permission.
+
+  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+  FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+  COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+  CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+  LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+  ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+  POSSIBILITY OF SUCH DAMAGE.
+
+  The above license is used as a license under copyright only.  Please
+  reference the OMA IPR Policy for patent licensing terms:
+  http://www.openmobilealliance.org/ipr.html
+
+-->
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+  <xs:element name="LWM2M">
+    <xs:complexType>
+      <xs:sequence>
+        <xs:element minOccurs="1" maxOccurs="unbounded" name="Object">
+          <xs:complexType>
+            <xs:sequence>
+              <xs:element name="Name" type="xs:string"/>
+              <xs:element name="Description1" type="xs:string"/>
+              <xs:element name="ObjectID" type="xs:unsignedShort"/>
+              <xs:element name="ObjectURN" type="xs:string"/>
+              <xs:element name="LWM2MVersion" type="xs:string" minOccurs="0"/>
+              <xs:element name="ObjectVersion" type="xs:string" minOccurs="0"/>			  
+              <xs:element name="MultipleInstances">
+                <xs:simpleType>
+                  <xs:restriction base="xs:string">
+                    <xs:enumeration value="Multiple"/>
+                    <xs:enumeration value="Single"/>
+                  </xs:restriction>
+                </xs:simpleType>
+              </xs:element>
+              <xs:element name="Mandatory">
+                <xs:simpleType>
+                  <xs:restriction base="xs:string">
+                    <xs:enumeration value="Mandatory"/>
+                    <xs:enumeration value="Optional"/>
+                  </xs:restriction>
+                </xs:simpleType>
+              </xs:element>
+              <xs:element name="Resources">
+                <xs:complexType>
+                  <xs:sequence>
+                    <xs:element maxOccurs="unbounded" name="Item">
+                      <xs:complexType>
+                        <xs:sequence>
+                          <xs:element name="Name" type="xs:string"/>
+                          <xs:element name="Operations">
+                            <xs:simpleType>
+                              <xs:restriction base="xs:string">
+                                <xs:enumeration value="R"/>
+                                <xs:enumeration value="W"/>
+                                <xs:enumeration value="RW"/>
+                                <xs:enumeration value="E"/>
+                                <xs:enumeration value=""/>
+                              </xs:restriction>
+                            </xs:simpleType>
+                          </xs:element>
+                          <xs:element name="MultipleInstances">
+                            <xs:simpleType>
+                              <xs:restriction base="xs:string">
+                                <xs:enumeration value="Multiple"/>
+                                <xs:enumeration value="Single"/>
+                              </xs:restriction>
+                            </xs:simpleType>
+                          </xs:element>
+                          <xs:element name="Mandatory">
+                            <xs:simpleType>
+                              <xs:restriction base="xs:string">
+                                <xs:enumeration value="Mandatory"/>
+                                <xs:enumeration value="Optional"/>
+                              </xs:restriction>
+                            </xs:simpleType>
+                          </xs:element>
+                          <xs:element name="Type">
+                            <xs:simpleType>
+                              <xs:restriction base="xs:string">
+                                <xs:enumeration value="String"/>
+                                <xs:enumeration value="Integer"/>
+                                <xs:enumeration value="Float"/>
+                                <xs:enumeration value="Boolean"/>
+                                <xs:enumeration value="Opaque"/>
+                                <xs:enumeration value="Time"/>
+                                <xs:enumeration value="Objlnk"/>
+                                <xs:enumeration value=""/>
+                              </xs:restriction>
+                            </xs:simpleType>
+                          </xs:element>
+                          <xs:element name="RangeEnumeration" type="xs:string"/>
+                          <xs:element name="Units" type="xs:string"/>
+                          <xs:element name="Description" type="xs:string"/>
+                        </xs:sequence>
+                        <xs:attribute name="ID" type="xs:unsignedShort" use="required"/>
+                      </xs:complexType>
+                    </xs:element>
+                  </xs:sequence>
+                </xs:complexType>
+              </xs:element>
+              <xs:element name="Description2" type="xs:string"/>
+            </xs:sequence>
+            <xs:attribute name="ObjectType" type="xs:string" use="required"/>
+          </xs:complexType>
+        </xs:element>
+      </xs:sequence>
+    </xs:complexType>
+  </xs:element>
+</xs:schema>
\ No newline at end of file
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
index 55b2018e..4db12632 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/LeshanServerDemo.java
@@ -91,7 +91,7 @@ public class LeshanServerDemo {
 
     // /!\ This field is a COPY of org.eclipse.leshan.client.demo.LeshanClientDemo.modelPaths /!\
     // TODO create a leshan-demo project ?
-    private final static String[] modelPaths = new String[] { "10241.xml", "10242.xml", "10243.xml", "10244.xml",
+    public final static String[] modelPaths = new String[] { "10241.xml", "10242.xml", "10243.xml", "10244.xml",
                             "10245.xml", "10246.xml", "10247.xml", "10248.xml", "10249.xml", "10250.xml", "10251.xml",
                             "10252.xml", "10253.xml", "10254.xml", "10255.xml", "10256.xml", "10257.xml", "10258.xml",
                             "10259.xml", "10260-2_0.xml", "10260.xml", "10262.xml", "10263.xml", "10264.xml",
@@ -523,7 +523,7 @@ public class LeshanServerDemo {
         List<ObjectModel> models = ObjectLoader.loadDefault();
         models.addAll(ObjectLoader.loadDdfResources("/models/", modelPaths));
         if (modelsFolderPath != null) {
-            models.addAll(ObjectLoader.loadObjectsFromDir(new File(modelsFolderPath)));
+            models.addAll(ObjectLoader.loadObjectsFromDir(new File(modelsFolderPath), true));
         }
         LwM2mModelProvider modelProvider = new VersionedModelProvider(models);
         builder.setObjectModelProvider(modelProvider);
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/model/Ddf2JsonGenerator.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/model/Ddf2JsonGenerator.java
index 8cb460cb..d6da80cd 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/model/Ddf2JsonGenerator.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/model/Ddf2JsonGenerator.java
@@ -28,6 +28,7 @@ import java.util.Comparator;
 import java.util.List;
 
 import org.eclipse.leshan.core.model.DDFFileParser;
+import org.eclipse.leshan.core.model.InvalidDDFFileException;
 import org.eclipse.leshan.core.model.ObjectModel;
 import org.eclipse.leshan.core.util.json.JsonException;
 
@@ -42,7 +43,7 @@ public class Ddf2JsonGenerator {
         }
     }
 
-    public void generate(File input, OutputStream output) throws IOException, JsonException {
+    public void generate(File input, OutputStream output) throws IOException, JsonException, InvalidDDFFileException {
         // check input exists
         if (!input.exists())
             throw new FileNotFoundException(input.toString());
@@ -60,7 +61,7 @@ public class Ddf2JsonGenerator {
         DDFFileParser ddfParser = new DDFFileParser();
         for (File f : files) {
             if (f.canRead()) {
-                objectModels.addAll(ddfParser.parse(f));
+                objectModels.addAll(ddfParser.parseEx(f));
             }
         }
 
@@ -76,7 +77,8 @@ public class Ddf2JsonGenerator {
         generate(objectModels, output);
     }
 
-    public static void main(String[] args) throws FileNotFoundException, IOException, JsonException {
+    public static void main(String[] args)
+            throws FileNotFoundException, IOException, JsonException, InvalidDDFFileException {
         // default value
         String ddfFilesPath = DEFAULT_DDF_FILES_PATH;
         String outputPath = DEFAULT_OUTPUT_PATH;
diff --git a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/model/DdfList2JsonGenerator.java b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/model/DdfList2JsonGenerator.java
index c9c0dfed..ad78da99 100644
--- a/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/model/DdfList2JsonGenerator.java
+++ b/leshan-server-demo/src/main/java/org/eclipse/leshan/server/demo/model/DdfList2JsonGenerator.java
@@ -32,6 +32,7 @@ import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
 
+import org.eclipse.leshan.core.model.InvalidDDFFileException;
 import org.eclipse.leshan.core.util.json.JsonException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -105,7 +106,7 @@ public class DdfList2JsonGenerator {
         }
     }
 
-    public static void main(String[] args) throws IOException, JsonException {
+    public static void main(String[] args) throws IOException, JsonException, InvalidDDFFileException {
         // default values
         String ddfFilesPath = Ddf2JsonGenerator.DEFAULT_DDF_FILES_PATH;
         String outputPath = Ddf2JsonGenerator.DEFAULT_OUTPUT_PATH;


Test patch:
diff --git a/leshan-client-demo/src/test/java/org/eclipse/leshan/client/demo/model/ValidateClientDemoModelsTest.java b/leshan-client-demo/src/test/java/org/eclipse/leshan/client/demo/model/ValidateClientDemoModelsTest.java
new file mode 100644
index 00000000..5fc11628
--- /dev/null
+++ b/leshan-client-demo/src/test/java/org/eclipse/leshan/client/demo/model/ValidateClientDemoModelsTest.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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.client.demo.model;
+
+import java.io.IOException;
+
+import org.eclipse.leshan.client.demo.LeshanClientDemo;
+import org.eclipse.leshan.core.model.InvalidDDFFileException;
+import org.eclipse.leshan.core.model.InvalidModelException;
+import org.eclipse.leshan.core.model.ObjectLoader;
+import org.junit.Test;
+
+public class ValidateClientDemoModelsTest {
+
+    @Test
+    public void validate_embedded_models() throws InvalidModelException, InvalidDDFFileException, IOException {
+        ObjectLoader.loadDdfResources("/models/", LeshanClientDemo.modelPaths, true);
+    }
+}
diff --git a/leshan-core/src/test/java/org/eclipse/leshan/core/model/ValidateModelsTest.java b/leshan-core/src/test/java/org/eclipse/leshan/core/model/ValidateModelsTest.java
new file mode 100644
index 00000000..7c4d4a36
--- /dev/null
+++ b/leshan-core/src/test/java/org/eclipse/leshan/core/model/ValidateModelsTest.java
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * 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.model;
+
+import java.io.IOException;
+
+import org.junit.Test;
+
+public class ValidateModelsTest {
+
+    @Test
+    public void validate_embedded_models() throws InvalidModelException, InvalidDDFFileException, IOException {
+        ObjectLoader.loadDdfResources("/models/", ObjectLoader.ddfpaths, true);
+    }
+}
diff --git a/leshan-server-demo/src/test/java/org/eclipse/leshan/server/demo/model/ValidateServerDemoModelsTest.java b/leshan-server-demo/src/test/java/org/eclipse/leshan/server/demo/model/ValidateServerDemoModelsTest.java
new file mode 100644
index 00000000..f1ab317d
--- /dev/null
+++ b/leshan-server-demo/src/test/java/org/eclipse/leshan/server/demo/model/ValidateServerDemoModelsTest.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * 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.server.demo.model;
+
+import java.io.IOException;
+
+import org.eclipse.leshan.core.model.InvalidDDFFileException;
+import org.eclipse.leshan.core.model.InvalidModelException;
+import org.eclipse.leshan.core.model.ObjectLoader;
+import org.eclipse.leshan.server.demo.LeshanServerDemo;
+import org.junit.Test;
+
+public class ValidateServerDemoModelsTest {
+
+    @Test
+    public void validate_embedded_models() throws InvalidModelException, InvalidDDFFileException, IOException {
+        ObjectLoader.loadDdfResources("/models/", LeshanServerDemo.modelPaths, true);
+    }
+}