You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by jk...@apache.org on 2022/05/23 16:00:04 UTC

[unomi] branch master updated: UNOMI-571: JSON Schema extensions system (#426)

This is an automated email from the ASF dual-hosted git repository.

jkevan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/master by this push:
     new dcc6cd8d2 UNOMI-571: JSON Schema extensions system (#426)
dcc6cd8d2 is described below

commit dcc6cd8d24fa55edb3b2c745f27ed744289e4f88
Author: kevan Jahanshahi <ke...@jahia.com>
AuthorDate: Mon May 23 17:59:58 2022 +0200

    UNOMI-571: JSON Schema extensions system (#426)
    
    * UNOMI-571: Fix concurrency issue, cache issue in JSON Schema and provide better tests coverage
    
    * Fix tests
    
    * JSON Schema extension implem
    
    * Fix tests + new tests for extensions
    
    * try to Fix tests
    
    * try to Fix tests
    
    * try to Fix tests
    
    * try to Fix tests
    
    * code review adjustments
    
    * code review adjustments
    
    * code review adjustments
---
 .../unomi/schema/rest/JsonSchemaEndPoint.java      |  20 +-
 .../apache/unomi/schema/api/JsonSchemaWrapper.java |  51 ++--
 .../org/apache/unomi/schema/api/SchemaService.java |  25 +-
 .../unomi/schema/impl/SchemaServiceImpl.java       | 289 ++++++++++++++-------
 .../unomi/schema/listener/JsonSchemaListener.java  |  16 --
 .../resources/OSGI-INF/blueprint/blueprint.xml     |   6 +-
 .../test/java/org/apache/unomi/itests/BaseIT.java  |  11 +-
 .../org/apache/unomi/itests/ContextServletIT.java  |  42 +--
 .../org/apache/unomi/itests/InputValidationIT.java |  52 +++-
 .../java/org/apache/unomi/itests/JSONSchemaIT.java | 182 ++++++++++---
 .../resources/schemas/event-dummy-extended-2.json  |  11 +
 .../resources/schemas/event-dummy-extended.json    |  10 +
 .../resources/schemas/event-dummy-invalid-1.json   |   9 +
 .../resources/schemas/event-dummy-invalid-2.json   |   9 +
 .../resources/schemas/event-dummy-invalid-3.json   |   8 +
 .../test/resources/schemas/event-dummy-valid.json  |   8 +
 .../resources/schemas/events/dummy-event-type.json |  43 ---
 .../schemas/events/negative-test-event-type.json   |  13 -
 .../resources/schemas/schema-dummy-extension.json  |  18 ++
 .../schema-dummy-properties-extension-2.json       |  21 ++
 .../schemas/schema-dummy-properties-extension.json |  18 ++
 .../schemas/schema-dummy-properties-updated.json   |  24 ++
 .../resources/schemas/schema-dummy-properties.json |  22 ++
 .../src/test/resources/schemas/schema-dummy.json   |  22 ++
 ...-invalid-name.json => schema-invalid-name.json} |   0
 .../test-invalid.json => schema-invalid.json}      |   0
 ...ined-event-type.json => schema-predefined.json} |   4 +-
 .../eventcollector_invalidSessionId.json           |  12 +-
 .../eventcollector_request_size_invalid.json       |  12 +-
 .../eventcollector_request_size_valid.json         |  12 +-
 .../resources/validation/eventcollector_valid.json |  12 +-
 kar/pom.xml                                        |   5 +
 32 files changed, 653 insertions(+), 334 deletions(-)

diff --git a/extensions/json-schema/rest/src/main/java/org/apache/unomi/schema/rest/JsonSchemaEndPoint.java b/extensions/json-schema/rest/src/main/java/org/apache/unomi/schema/rest/JsonSchemaEndPoint.java
index 26f09222f..6c9fb021d 100644
--- a/extensions/json-schema/rest/src/main/java/org/apache/unomi/schema/rest/JsonSchemaEndPoint.java
+++ b/extensions/json-schema/rest/src/main/java/org/apache/unomi/schema/rest/JsonSchemaEndPoint.java
@@ -18,7 +18,6 @@
 package org.apache.unomi.schema.rest;
 
 import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
-import org.apache.unomi.api.Metadata;
 import org.apache.unomi.rest.exception.InvalidRequestException;
 import org.apache.unomi.schema.api.SchemaService;
 import org.osgi.service.component.annotations.Component;
@@ -32,7 +31,7 @@ import javax.ws.rs.*;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 import java.util.Base64;
-import java.util.List;
+import java.util.Set;
 
 @WebService
 @Produces(MediaType.APPLICATION_JSON + ";charset=UTF-8")
@@ -56,21 +55,14 @@ public class JsonSchemaEndPoint {
     }
 
     /**
-     * Retrieves the 50 first json schema metadatas by default.
+     * Get the list of installed Json Schema Ids
      *
-     * @param offset zero or a positive integer specifying the position of the first element in the total ordered collection of matching elements
-     * @param size   a positive integer specifying how many matching elements should be retrieved or {@code -1} if all of them should be retrieved
-     * @param sortBy an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering
-     *               elements according to the property order in the
-     *               String, considering each in turn and moving on to the next one in case of equality of all preceding ones. Each property name is optionally followed by
-     *               a column ({@code :}) and an order specifier: {@code asc} or {@code desc}.
-     * @return a List of the 50 first json schema metadata
+     * @return A Set of JSON schema ids
      */
     @GET
     @Path("/")
-    public List<Metadata> getJsonSchemaMetadatas(@QueryParam("offset") @DefaultValue("0") int offset,
-            @QueryParam("size") @DefaultValue("50") int size, @QueryParam("sort") String sortBy) {
-        return schemaService.getJsonSchemaMetadatas(offset, size, sortBy).getList();
+    public Set<String> getInstalledJsonSchemaIds() {
+        return schemaService.getInstalledJsonSchemaIds();
     }
 
     /**
@@ -81,7 +73,7 @@ public class JsonSchemaEndPoint {
      */
     @POST
     @Path("/")
-    @Consumes(MediaType.TEXT_PLAIN)
+    @Consumes({MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON})
     @Produces(MediaType.APPLICATION_JSON)
     public Response save(String jsonSchema) {
         try {
diff --git a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/JsonSchemaWrapper.java b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/JsonSchemaWrapper.java
index ce123879e..d8efe08f5 100644
--- a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/JsonSchemaWrapper.java
+++ b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/JsonSchemaWrapper.java
@@ -17,8 +17,10 @@
 
 package org.apache.unomi.schema.api;
 
-import org.apache.unomi.api.Metadata;
-import org.apache.unomi.api.MetadataItem;
+import org.apache.unomi.api.Item;
+import org.apache.unomi.api.TimestampedItem;
+
+import java.util.Date;
 
 /**
  * Object which represents a JSON schema, it's a wrapper because it contains some additional info used by the
@@ -26,35 +28,35 @@ import org.apache.unomi.api.MetadataItem;
  * The JSON schema is store as String to avoid transformation during JSON schema resolution in the Unomi SchemaService.
  * Also, it's extending  MetadataItem so that it can be persisted like that in Unomi storage system.
  */
-public class JsonSchemaWrapper extends MetadataItem {
+public class JsonSchemaWrapper extends Item implements TimestampedItem {
     public static final String ITEM_TYPE = "jsonSchema";
 
-    private String id;
     private String schema;
     private String target;
+    private String extendsSchemaId;
+    private Date timeStamp;
 
-    public JsonSchemaWrapper(){}
+    /**
+     * Instantiates a new JSON schema
+     */
+    public JsonSchemaWrapper() {
+    }
 
     /**
-     * Instantiates a new JSON schema with an id and a schema as string
+     * Instantiates a new JSON schema
      *
      * @param id     id of the schema
      * @param schema as string
-     * @param target of the schema
+     * @param target of the schema (optional)
+     * @param extendsSchemaId is the URI of another Schema to be extended by current one. (optional)
+     * @param timeStamp of the schema
      */
-    public JsonSchemaWrapper(String id, String schema, String target) {
-        super(new Metadata(id));
-        this.id = id;
+    public JsonSchemaWrapper(String id, String schema, String target, String extendsSchemaId, Date timeStamp) {
+        super(id);
         this.schema = schema;
         this.target = target;
-    }
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
+        this.extendsSchemaId = extendsSchemaId;
+        this.timeStamp = timeStamp;
     }
 
     public String getSchema() {
@@ -72,4 +74,17 @@ public class JsonSchemaWrapper extends MetadataItem {
     public void setTarget(String target) {
         this.target = target;
     }
+
+    public String getExtendsSchemaId() {
+        return extendsSchemaId;
+    }
+
+    public void setExtendsSchemaId(String extendsSchemaId) {
+        this.extendsSchemaId = extendsSchemaId;
+    }
+
+    @Override
+    public Date getTimeStamp() {
+        return timeStamp;
+    }
 }
\ No newline at end of file
diff --git a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/SchemaService.java b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/SchemaService.java
index c479c4015..34c11e7d5 100644
--- a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/SchemaService.java
+++ b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/api/SchemaService.java
@@ -17,32 +17,16 @@
 
 package org.apache.unomi.schema.api;
 
-import org.apache.unomi.api.Metadata;
-import org.apache.unomi.api.PartialList;
-
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Service that allow to manage JSON schema. It allows to get, save and delete schemas
  */
 public interface SchemaService {
 
-    /**
-     * Retrieves json schema metadatas, ordered according to the specified {@code sortBy} String and and paged: only {@code size} of them
-     * are retrieved, starting with the {@code
-     * offset}-th one.
-     *
-     * @param offset zero or a positive integer specifying the position of the first element in the total ordered collection of matching elements
-     * @param size   a positive integer specifying how many matching elements should be retrieved or {@code -1} if all of them should be retrieved
-     * @param sortBy an optional ({@code null} if no sorting is required) String of comma ({@code ,}) separated property names on which ordering should be performed, ordering elements according to the property order in the
-     *               String, considering each in turn and moving on to the next one in case of equality of all preceding ones. Each property name is optionally followed by
-     *               a column ({@code :}) and an order specifier: {@code asc} or {@code desc}.
-     * @return a {@link PartialList} of json schema metadata
-     */
-    PartialList<Metadata> getJsonSchemaMetadatas(int offset, int size, String sortBy);
-
     /**
      * Verify if a jsonNode is valid against a schema
      *
@@ -52,6 +36,13 @@ public interface SchemaService {
      */
     boolean isValid(String data, String schemaId);
 
+    /**
+     * Get the list of installed Json Schema Ids
+     *
+     * @return A Set of JSON schema ids
+     */
+    Set<String> getInstalledJsonSchemaIds();
+
     /**
      * Get a schema matching by a schema id
      *
diff --git a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/impl/SchemaServiceImpl.java b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/impl/SchemaServiceImpl.java
index e91d29776..44d3850c8 100644
--- a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/impl/SchemaServiceImpl.java
+++ b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/impl/SchemaServiceImpl.java
@@ -20,26 +20,24 @@ package org.apache.unomi.schema.impl;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.MissingNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.networknt.schema.*;
-import com.networknt.schema.uri.URIFetcher;
 import org.apache.commons.io.IOUtils;
-import org.apache.unomi.api.Metadata;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.services.SchedulerService;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.api.Item;
 import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.schema.api.JsonSchemaWrapper;
 import org.apache.unomi.schema.api.SchemaService;
-import org.osgi.framework.BundleContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
-import java.net.URISyntaxException;
 import java.util.*;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
 import java.util.stream.Collectors;
 
 public class SchemaServiceImpl implements SchemaService {
@@ -50,86 +48,96 @@ public class SchemaServiceImpl implements SchemaService {
 
     ObjectMapper objectMapper = new ObjectMapper();
 
-    private final Map<String, JsonSchemaWrapper> predefinedUnomiJSONSchemaById = new HashMap<>();
-    private Map<String, JsonSchemaWrapper> schemasById = new HashMap<>();
+    /**
+     *  Schemas provided by Unomi runtime bundles in /META-INF/cxs/schemas/...
+     */
+    private final ConcurrentMap<String, JsonSchemaWrapper> predefinedUnomiJSONSchemaById = new ConcurrentHashMap<>();
+    /**
+     * All Unomi schemas indexed by URI
+     */
+    private ConcurrentMap<String, JsonSchemaWrapper> schemasById = new ConcurrentHashMap<>();
+    /**
+     * Available extensions indexed by key:schema URI to be extended, value: list of schema extension URIs
+     */
+    private ConcurrentMap<String, Set<String>> extensions = new ConcurrentHashMap<>();
 
     private Integer jsonSchemaRefreshInterval = 1000;
     private ScheduledFuture<?> scheduledFuture;
 
-    private BundleContext bundleContext;
     private PersistenceService persistenceService;
-    private SchedulerService schedulerService;
     private JsonSchemaFactory jsonSchemaFactory;
 
+    // TODO UNOMI-572: when fixing UNOMI-572 please remove the usage of the custom ScheduledExecutorService and re-introduce the Unomi Scheduler Service
+    private ScheduledExecutorService scheduler;
+    //private SchedulerService schedulerService;
 
-    @Override
-    public PartialList<Metadata> getJsonSchemaMetadatas(int offset, int size, String sortBy) {
-        PartialList<JsonSchemaWrapper> items = persistenceService.getAllItems(JsonSchemaWrapper.class, offset, size, sortBy);
-        List<Metadata> details = new LinkedList<>();
-        for (JsonSchemaWrapper definition : items.getList()) {
-            details.add(definition.getMetadata());
-        }
-        return new PartialList<>(details, items.getOffset(), items.getPageSize(), items.getTotalSize(), items.getTotalSizeRelation());
-    }
 
     @Override
     public boolean isValid(String data, String schemaId) {
-        JsonSchema jsonSchema = null;
-        JsonNode jsonNode = null;
+        JsonSchema jsonSchema;
+        JsonNode jsonNode;
 
         try {
             jsonNode = objectMapper.readTree(data);
             jsonSchema = jsonSchemaFactory.getSchema(new URI(schemaId));
         } catch (Exception e) {
-            logger.error("Failed to process data to validate because {} - Set SchemaServiceImpl at DEBUG level for more detail ", e.getMessage());
-            logger.debug("full error",e);
+            logger.debug("Schema validation failed", e);
             return false;
         }
 
         if (jsonNode == null) {
-            logger.warn("No data to validate");
+            logger.debug("Schema validation failed because: no data to validate");
             return false;
         }
 
         if (jsonSchema == null) {
-            logger.warn("No schema found for {}", schemaId);
+            logger.debug("Schema validation failed because: Schema not found {}", schemaId);
+            return false;
+        }
+
+        Set<ValidationMessage> validationMessages;
+        try {
+            validationMessages = jsonSchema.validate(jsonNode);
+        } catch (Exception e) {
+            logger.debug("Schema validation failed", e);
             return false;
         }
 
-        Set<ValidationMessage> validationMessages = jsonSchema.validate(jsonNode);
         if (validationMessages == null || validationMessages.isEmpty()) {
             return true;
+        } else {
+            if (logger.isDebugEnabled()) {
+                logger.debug("Schema validation found {} errors while validating against schema: {}", validationMessages.size(), schemaId);
+                for (ValidationMessage validationMessage : validationMessages) {
+                    logger.debug("Validation error: {}", validationMessage);
+                }
+            }
+            return false;
         }
-        for (ValidationMessage validationMessage : validationMessages) {
-            logger.error("Error validating object against schema {}: {}", schemaId, validationMessage);
-        }
-        return false;
+    }
+
+    @Override
+    public JsonSchemaWrapper getSchema(String schemaId) {
+        return schemasById.get(schemaId);
+    }
+
+    @Override
+    public Set<String> getInstalledJsonSchemaIds() {
+        return schemasById.keySet();
     }
 
     @Override
     public List<JsonSchemaWrapper> getSchemasByTarget(String target) {
-        return schemasById.values().stream().filter(jsonSchemaWrapper -> jsonSchemaWrapper.getTarget() != null && jsonSchemaWrapper.getTarget().equals(target))
+        return schemasById.values().stream()
+                .filter(jsonSchemaWrapper -> jsonSchemaWrapper.getTarget() != null && jsonSchemaWrapper.getTarget().equals(target))
                 .collect(Collectors.toList());
     }
 
     @Override
     public void saveSchema(String schema) {
-        JsonSchema jsonSchema = jsonSchemaFactory.getSchema(schema);
-        JsonNode schemaNode = jsonSchema.getSchemaNode();
-        String id = schemaNode.get("$id").asText();
-
-        if (!predefinedUnomiJSONSchemaById.containsKey(id)) {
-            String target = schemaNode.at("/self/target").asText();
-            String name = schemaNode.at("/self/name").asText();
-
-            if ("events".equals(target) && !name.matches("[_A-Za-z][_0-9A-Za-z]*")) {
-                throw new IllegalArgumentException(
-                        "The \"/self/name\" value should match the following regular expression [_A-Za-z][_0-9A-Za-z]* for the Json schema on events");
-            }
-
-            JsonSchemaWrapper jsonSchemaWrapper = new JsonSchemaWrapper(id, schema, target);
+        JsonSchemaWrapper jsonSchemaWrapper = buildJsonSchemaWrapper(schema);
+        if (!predefinedUnomiJSONSchemaById.containsKey(jsonSchemaWrapper.getItemId())) {
             persistenceService.save(jsonSchemaWrapper);
-            schemasById.put(id, jsonSchemaWrapper);
         } else {
             throw new IllegalArgumentException("Trying to save a Json Schema that is using the ID of an existing Json Schema provided by Unomi is forbidden");
         }
@@ -139,7 +147,7 @@ public class SchemaServiceImpl implements SchemaService {
     public boolean deleteSchema(String schemaId) {
         // forbidden to delete predefined Unomi schemas
         if (!predefinedUnomiJSONSchemaById.containsKey(schemaId)) {
-            schemasById.remove(schemaId);
+            // remove persisted schema
             return persistenceService.remove(schemaId, JsonSchemaWrapper.class);
         }
         return false;
@@ -147,79 +155,174 @@ public class SchemaServiceImpl implements SchemaService {
 
     @Override
     public void loadPredefinedSchema(InputStream schemaStream) throws IOException {
-        String jsonSchema = IOUtils.toString(schemaStream);
-
-        // check that schema is valid and get the id
-        JsonNode schemaNode = jsonSchemaFactory.getSchema(jsonSchema).getSchemaNode();
-        String schemaId = schemaNode.get("$id").asText();
-        String target = schemaNode.at("/self/target").asText();
-        JsonSchemaWrapper jsonSchemaWrapper = new JsonSchemaWrapper(schemaId, jsonSchema, target);
-
-        predefinedUnomiJSONSchemaById.put(schemaId, jsonSchemaWrapper);
-        schemasById.put(schemaId, jsonSchemaWrapper);
+        String schema = IOUtils.toString(schemaStream);
+        JsonSchemaWrapper jsonSchemaWrapper = buildJsonSchemaWrapper(schema);
+        predefinedUnomiJSONSchemaById.put(jsonSchemaWrapper.getItemId(), jsonSchemaWrapper);
     }
 
     @Override
     public boolean unloadPredefinedSchema(InputStream schemaStream) {
         JsonNode schemaNode = jsonSchemaFactory.getSchema(schemaStream).getSchemaNode();
         String schemaId = schemaNode.get("$id").asText();
+        return predefinedUnomiJSONSchemaById.remove(schemaId) != null;
+    }
 
-        return predefinedUnomiJSONSchemaById.remove(schemaId) != null && schemasById.remove(schemaId) != null;
+    private JsonSchemaWrapper buildJsonSchemaWrapper(String schema) {
+        JsonSchema jsonSchema = jsonSchemaFactory.getSchema(schema);
+        JsonNode schemaNode = jsonSchema.getSchemaNode();
+
+        String schemaId = schemaNode.get("$id").asText();
+        String target = schemaNode.at("/self/target").asText();
+        String name = schemaNode.at("/self/name").asText();
+        String extendsSchemaId = schemaNode.at("/self/extends").asText();
+
+        if ("events".equals(target) && !name.matches("[_A-Za-z][_0-9A-Za-z]*")) {
+            throw new IllegalArgumentException(
+                    "The \"/self/name\" value should match the following regular expression [_A-Za-z][_0-9A-Za-z]* for the Json schema on events");
+        }
+
+        return new JsonSchemaWrapper(schemaId, schema, target, extendsSchemaId, new Date());
     }
 
-    @Override
-    public JsonSchemaWrapper getSchema(String schemaId) {
-        return schemasById.get(schemaId);
+    private void refreshJSONSchemas() {
+        // use local variable to avoid concurrency issues.
+        Map<String, JsonSchemaWrapper> schemasByIdReloaded = new HashMap<>();
+        schemasByIdReloaded.putAll(predefinedUnomiJSONSchemaById);
+        schemasByIdReloaded.putAll(persistenceService.getAllItems(JsonSchemaWrapper.class).stream().collect(Collectors.toMap(Item::getItemId, s -> s)));
+
+        // flush cache if size is different (can be new schema or deleted schemas)
+        boolean changes = schemasByIdReloaded.size() != schemasById.size();
+        // check for modifications
+        if (!changes) {
+            for (JsonSchemaWrapper reloadedSchema : schemasByIdReloaded.values()) {
+                JsonSchemaWrapper oldSchema = schemasById.get(reloadedSchema.getItemId());
+                if (oldSchema == null || !oldSchema.getTimeStamp().equals(reloadedSchema.getTimeStamp())) {
+                    changes = true;
+                    break;
+                }
+            }
+        }
+
+        if (changes) {
+            schemasById = new ConcurrentHashMap<>(schemasByIdReloaded);
+
+            initExtensions(schemasByIdReloaded);
+            initJsonSchemaFactory();
+        }
     }
 
-    private URIFetcher getUriFetcher() {
-        return uri -> {
-            logger.debug("Fetching schema {}", uri);
-            JsonSchemaWrapper jsonSchemaWrapper = schemasById.get(uri.toString());
-            if (jsonSchemaWrapper == null) {
-                logger.error("Couldn't find schema {}", uri);
-                return null;
+    private void initExtensions(Map<String, JsonSchemaWrapper> schemas) {
+        Map<String, Set<String>> extensionsReloaded = new HashMap<>();
+        // lookup extensions
+        List<JsonSchemaWrapper> schemaExtensions = schemas.values()
+                .stream()
+                .filter(jsonSchemaWrapper -> StringUtils.isNotBlank(jsonSchemaWrapper.getExtendsSchemaId()))
+                .collect(Collectors.toList());
+
+        // build new in RAM extensions map
+        for (JsonSchemaWrapper extension : schemaExtensions) {
+            String extendedSchemaId = extension.getExtendsSchemaId();
+            if (!extension.getItemId().equals(extendedSchemaId)) {
+                if (!extensionsReloaded.containsKey(extendedSchemaId)) {
+                    extensionsReloaded.put(extendedSchemaId, new HashSet<>());
+                }
+                extensionsReloaded.get(extendedSchemaId).add(extension.getItemId());
+            } else {
+                logger.warn("A schema cannot extends himself, please fix your schema definition for schema: {}", extendedSchemaId);
             }
-            return IOUtils.toInputStream(jsonSchemaWrapper.getSchema());
-        };
+        }
+
+        extensions = new ConcurrentHashMap<>(extensionsReloaded);
     }
 
-    private void refreshJSONSchemas() {
-        schemasById = new HashMap<>();
-        schemasById.putAll(predefinedUnomiJSONSchemaById);
+    private String generateExtendedSchema(String id, String schema) throws JsonProcessingException {
+        Set<String> extensionIds = extensions.get(id);
+        if (extensionIds != null && extensionIds.size() > 0) {
+            // This schema need to be extends !
+            ObjectNode jsonSchema = (ObjectNode) objectMapper.readTree(schema);
+            ArrayNode allOf;
+            if (jsonSchema.at("/allOf") instanceof MissingNode) {
+                allOf = objectMapper.createArrayNode();
+            } else if (jsonSchema.at("/allOf") instanceof ArrayNode){
+                allOf = (ArrayNode) jsonSchema.at("/allOf");
+            } else {
+                logger.warn("Cannot extends schema allOf property, it should be an Array, please fix your schema definition for schema: {}", id);
+                return schema;
+            }
+
+            // Add each extension URIs as new ref in the allOf
+            for (String extensionId : extensionIds) {
+                ObjectNode newAllOf = objectMapper.createObjectNode();
+                newAllOf.put("$ref", extensionId);
+                allOf.add(newAllOf);
+            }
 
-        persistenceService.getAllItems(JsonSchemaWrapper.class).forEach(
-                JsonSchemaWrapper -> schemasById.put(JsonSchemaWrapper.getId(), JsonSchemaWrapper));
+            // generate new extended schema as String
+            jsonSchema.putArray("allOf").addAll(allOf);
+            return objectMapper.writeValueAsString(jsonSchema);
+        }
+        return schema;
     }
 
-    private void initializeTimers() {
+    private void initPersistenceIndex() {
+        if (persistenceService.createIndex(JsonSchemaWrapper.ITEM_TYPE)) {
+            logger.info("{} index created", JsonSchemaWrapper.ITEM_TYPE);
+        } else {
+            logger.info("{} index already exists", JsonSchemaWrapper.ITEM_TYPE);
+        }
+    }
+
+    private void initTimers() {
         TimerTask task = new TimerTask() {
             @Override
             public void run() {
-                refreshJSONSchemas();
+                try {
+                    refreshJSONSchemas();
+                } catch (Exception e) {
+                    logger.error("Error while refreshing JSON Schemas", e);
+                }
             }
         };
-        scheduledFuture = schedulerService.getScheduleExecutorService()
-                .scheduleWithFixedDelay(task, 0, jsonSchemaRefreshInterval, TimeUnit.MILLISECONDS);
+        scheduledFuture = scheduler.scheduleWithFixedDelay(task, 0, jsonSchemaRefreshInterval, TimeUnit.MILLISECONDS);
     }
 
-    public void init() {
-        JsonMetaSchema jsonMetaSchema = JsonMetaSchema.builder(URI, JsonMetaSchema.getV201909())
-                .addKeyword(new NonValidationKeyword("self"))
-                .build();
-
+    private void initJsonSchemaFactory() {
         jsonSchemaFactory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909))
-                .addMetaSchema(jsonMetaSchema)
+                .addMetaSchema(JsonMetaSchema.builder(URI, JsonMetaSchema.getV201909())
+                        .addKeyword(new NonValidationKeyword("self"))
+                        .build())
                 .defaultMetaSchemaURI(URI)
-                .uriFetcher(getUriFetcher(), "https", "http")
+                .uriFetcher(uri -> {
+                    logger.debug("Fetching schema {}", uri);
+                    String schemaId = uri.toString();
+                    JsonSchemaWrapper jsonSchemaWrapper = getSchema(schemaId);
+                    if (jsonSchemaWrapper == null) {
+                        logger.error("Couldn't find schema {}", uri);
+                        return null;
+                    }
+
+                    String schema = jsonSchemaWrapper.getSchema();
+                    // Check if schema need to be extended
+                    schema = generateExtendedSchema(schemaId, schema);
+
+                    return IOUtils.toInputStream(schema);
+                }, "https", "http")
                 .build();
+    }
 
-        initializeTimers();
+    public void init() {
+        scheduler = Executors.newSingleThreadScheduledExecutor();
+        initPersistenceIndex();
+        initJsonSchemaFactory();
+        initTimers();
         logger.info("Schema service initialized.");
     }
 
     public void destroy() {
         scheduledFuture.cancel(true);
+        if (scheduler != null) {
+            scheduler.shutdown();
+        }
         logger.info("Schema service shutdown.");
     }
 
@@ -227,15 +330,7 @@ public class SchemaServiceImpl implements SchemaService {
         this.persistenceService = persistenceService;
     }
 
-    public void setSchedulerService(SchedulerService schedulerService) {
-        this.schedulerService = schedulerService;
-    }
-
     public void setJsonSchemaRefreshInterval(Integer jsonSchemaRefreshInterval) {
         this.jsonSchemaRefreshInterval = jsonSchemaRefreshInterval;
     }
-
-    public void setBundleContext(BundleContext bundleContext) {
-        this.bundleContext = bundleContext;
-    }
 }
diff --git a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/listener/JsonSchemaListener.java b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/listener/JsonSchemaListener.java
index 9e674d545..fda47dd85 100644
--- a/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/listener/JsonSchemaListener.java
+++ b/extensions/json-schema/services/src/main/java/org/apache/unomi/schema/listener/JsonSchemaListener.java
@@ -16,8 +16,6 @@
  */
 package org.apache.unomi.schema.listener;
 
-import org.apache.unomi.persistence.spi.PersistenceService;
-import org.apache.unomi.schema.api.JsonSchemaWrapper;
 import org.apache.unomi.schema.api.SchemaService;
 import org.osgi.framework.Bundle;
 import org.osgi.framework.BundleContext;
@@ -41,14 +39,9 @@ public class JsonSchemaListener implements SynchronousBundleListener {
     private static final Logger logger = LoggerFactory.getLogger(JsonSchemaListener.class.getName());
     public static final String ENTRIES_LOCATION = "META-INF/cxs/schemas";
 
-    private PersistenceService persistenceService;
     private SchemaService schemaService;
     private BundleContext bundleContext;
 
-    public void setPersistenceService(PersistenceService persistenceService) {
-        this.persistenceService = persistenceService;
-    }
-
     public void setSchemaService(SchemaService schemaService) {
         this.schemaService = schemaService;
     }
@@ -60,7 +53,6 @@ public class JsonSchemaListener implements SynchronousBundleListener {
     public void postConstruct() {
         logger.info("JSON schema listener initializing...");
         logger.debug("postConstruct {}", bundleContext.getBundle());
-        createIndexes();
 
         loadPredefinedSchemas(bundleContext, true);
 
@@ -106,14 +98,6 @@ public class JsonSchemaListener implements SynchronousBundleListener {
         }
     }
 
-    public void createIndexes() {
-        if (persistenceService.createIndex(JsonSchemaWrapper.ITEM_TYPE)) {
-            logger.info("{} index created", JsonSchemaWrapper.ITEM_TYPE);
-        } else {
-            logger.info("{} index already exists", JsonSchemaWrapper.ITEM_TYPE);
-        }
-    }
-
     private void loadPredefinedSchemas(BundleContext bundleContext, boolean load) {
         Enumeration<URL> predefinedSchemas = bundleContext.getBundle().findEntries(ENTRIES_LOCATION, "*.json", true);
         if (predefinedSchemas == null) {
diff --git a/extensions/json-schema/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/extensions/json-schema/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index 02e3280c0..88485b84b 100644
--- a/extensions/json-schema/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/extensions/json-schema/services/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -29,20 +29,18 @@
 
     <reference id="profileService" interface="org.apache.unomi.api.services.ProfileService"/>
     <reference id="persistenceService" interface="org.apache.unomi.persistence.spi.PersistenceService"/>
-    <reference id="schedulerService" interface="org.apache.unomi.api.services.SchedulerService"/>
+    <!--reference id="schedulerService" interface="org.apache.unomi.api.services.SchedulerService"/-->
 
     <bean id="schemaServiceImpl" class="org.apache.unomi.schema.impl.SchemaServiceImpl" init-method="init"
           destroy-method="destroy">
-        <property name="bundleContext" ref="blueprintBundleContext"/>
         <property name="persistenceService" ref="persistenceService"/>
-        <property name="schedulerService" ref="schedulerService"/>
+        <!--property name="schedulerService" ref="schedulerService"/-->
         <property name="jsonSchemaRefreshInterval" value="${json.schema.refresh.interval}"/>
     </bean>
     <service id="schemaService" ref="schemaServiceImpl" interface="org.apache.unomi.schema.api.SchemaService"/>
 
     <bean id="jsonSchemaListenerImpl" class="org.apache.unomi.schema.listener.JsonSchemaListener"
           init-method="postConstruct" destroy-method="preDestroy">
-        <property name="persistenceService" ref="persistenceService"/>
         <property name="bundleContext" ref="blueprintBundleContext"/>
         <property name="schemaService" ref="schemaServiceImpl"/>
     </bean>
diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
index cece1ce78..6a2591c40 100644
--- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java
@@ -44,6 +44,7 @@ import org.apache.unomi.api.services.RulesService;
 import org.apache.unomi.lifecycle.BundleWatcher;
 import org.apache.unomi.persistence.spi.CustomObjectMapper;
 import org.apache.unomi.persistence.spi.PersistenceService;
+import org.apache.unomi.schema.api.SchemaService;
 import org.junit.After;
 import org.junit.Assert;
 import org.junit.Before;
@@ -112,7 +113,6 @@ public abstract class BaseIT {
     protected static final int REQUEST_TIMEOUT = 60000;
     protected static final int DEFAULT_TRYING_TIMEOUT = 2000;
     protected static final int DEFAULT_TRYING_TRIES = 30;
-    private final static String JSONSCHEMA_URL = "/cxs/jsonSchema";
 
     protected final static ObjectMapper objectMapper;
 
@@ -605,13 +605,4 @@ public abstract class BaseIT {
             LOGGER.error("Could not close httpClient: " + httpClient, e);
         }
     }
-
-    void registerEventType(String jsonSchemaFileName) {
-        post(JSONSCHEMA_URL, "schemas/events/" + jsonSchemaFileName, ContentType.TEXT_PLAIN);
-    }
-
-    void unRegisterEventType(String jsonSchemaId) {
-        delete(JSONSCHEMA_URL + "/" + Base64.getEncoder().encodeToString(jsonSchemaId.getBytes()));
-    }
-
 }
diff --git a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
index 4e1b75957..90396ea14 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
@@ -36,6 +36,7 @@ import org.apache.unomi.api.services.ProfileService;
 import org.apache.unomi.api.services.SegmentService;
 import org.apache.unomi.persistence.spi.CustomObjectMapper;
 import org.apache.unomi.persistence.spi.PersistenceService;
+import org.apache.unomi.schema.api.SchemaService;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
@@ -80,9 +81,9 @@ public class ContextServletIT extends BaseIT {
 
     private final static String THIRD_PARTY_HEADER_NAME = "X-Unomi-Peer";
     private final static String TEST_EVENT_TYPE = "testEventType";
-    private final static String TEST_EVENT_TYPE_SCHEMA = "test-event-type.json";
+    private final static String TEST_EVENT_TYPE_SCHEMA = "schemas/events/test-event-type.json";
     private final static String FLOAT_PROPERTY_EVENT_TYPE = "floatPropertyType";
-    private final static String FLOAT_PROPERTY_EVENT_TYPE_SCHEMA = "float-property-type.json";
+    private final static String FLOAT_PROPERTY_EVENT_TYPE_SCHEMA = "schemas/events/float-property-type.json";
     private final static String TEST_PROFILE_ID = "test-profile-id";
 
     private final static String SEGMENT_ID = "test-segment-id";
@@ -111,12 +112,14 @@ public class ContextServletIT extends BaseIT {
     @Filter(timeout = 600000)
     protected SegmentService segmentService;
 
+    @Inject
+    @Filter(timeout = 600000)
+    protected SchemaService schemaService;
+
     private Profile profile;
 
     @Before
     public void setUp() throws InterruptedException {
-        this.registerEventType(TEST_EVENT_TYPE_SCHEMA);
-        this.registerEventType(FLOAT_PROPERTY_EVENT_TYPE_SCHEMA);
 
         //Create a past-event segment
         Metadata segmentMetadata = new Metadata(SEGMENT_ID);
@@ -136,29 +139,32 @@ public class ContextServletIT extends BaseIT {
         keepTrying("Profile " + TEST_PROFILE_ID + " not found in the required time", () -> profileService.load(TEST_PROFILE_ID),
                 Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
 
-        keepTrying("Couldn't find json schema endpoint", () -> get(JSONSCHEMA_URL, List.class), Objects::nonNull, DEFAULT_TRYING_TIMEOUT,
-                DEFAULT_TRYING_TRIES);
+        // create schemas required for tests
+        schemaService.saveSchema(resourceAsString(TEST_EVENT_TYPE_SCHEMA));
+        schemaService.saveSchema(resourceAsString(FLOAT_PROPERTY_EVENT_TYPE_SCHEMA));
+        keepTrying("Couldn't find json schemas",
+                () -> schemaService.getInstalledJsonSchemaIds(),
+                (schemaIds) -> (schemaIds.contains("https://unomi.apache.org/schemas/json/events/floatPropertyType/1-0-0") &&
+                        schemaIds.contains("https://unomi.apache.org/schemas/json/events/testEventType/1-0-0")),
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
     }
 
     @After
-    public void tearDown() {
+    public void tearDown() throws InterruptedException {
         TestUtils.removeAllEvents(definitionsService, persistenceService);
         TestUtils.removeAllSessions(definitionsService, persistenceService);
         TestUtils.removeAllProfiles(definitionsService, persistenceService);
         profileService.delete(profile.getItemId(), false);
         segmentService.removeSegmentDefinition(SEGMENT_ID, false);
 
-        String encodedString = Base64.getEncoder()
-                .encodeToString("https://unomi.apache.org/schemas/json/events/testEventType/1-0-0".getBytes());
-        delete(JSONSCHEMA_URL + "/" + encodedString);
-
-        encodedString = Base64.getEncoder()
-                .encodeToString("https://unomi.apache.org/schemas/json/events/floatPropertyType/1-0-0".getBytes());
-        delete(JSONSCHEMA_URL + "/" + encodedString);
-
-        encodedString = Base64.getEncoder()
-                .encodeToString("https://unomi.apache.org/schemas/json/events/floatPropertyType/1-0-0".getBytes());
-        delete(JSONSCHEMA_URL + "/" + encodedString);
+        // cleanup schemas
+        schemaService.deleteSchema("https://unomi.apache.org/schemas/json/events/testEventType/1-0-0");
+        schemaService.deleteSchema("https://unomi.apache.org/schemas/json/events/floatPropertyType/1-0-0");
+        keepTrying("Should not find json schemas anymore",
+                () -> schemaService.getInstalledJsonSchemaIds(),
+                (schemaIds) -> (!schemaIds.contains("https://unomi.apache.org/schemas/json/events/floatPropertyType/1-0-0") &&
+                        !schemaIds.contains("https://unomi.apache.org/schemas/json/events/testEventType/1-0-0")),
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
     }
 
     @Test
diff --git a/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java b/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
index 538b8b52a..bd6f4f869 100644
--- a/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/InputValidationIT.java
@@ -23,14 +23,18 @@ import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.entity.ContentType;
 import org.apache.http.entity.StringEntity;
 import org.apache.http.util.EntityUtils;
+import org.apache.unomi.api.Event;
 import org.apache.unomi.itests.tools.httpclient.HttpClientThatWaitsForUnomi;
-import org.junit.Before;
-import org.junit.Test;
+import org.apache.unomi.schema.api.JsonSchemaWrapper;
+import org.apache.unomi.schema.api.SchemaService;
+import org.junit.*;
 import org.junit.runner.RunWith;
 import org.ops4j.pax.exam.junit.PaxExam;
 import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
 import org.ops4j.pax.exam.spi.reactors.PerSuite;
+import org.ops4j.pax.exam.util.Filter;
 
+import javax.inject.Inject;
 import java.io.IOException;
 import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
@@ -51,6 +55,10 @@ public class InputValidationIT extends BaseIT {
     private final static String ERROR_MESSAGE_REQUEST_SIZE_LIMIT_EXCEEDED = "Request rejected by the server because: Request size exceed the limit";
     private final static String ERROR_MESSAGE_INVALID_DATA_RECEIVED = "Request rejected by the server because: Invalid received data";
 
+    @Inject
+    @Filter(timeout = 600000)
+    protected SchemaService schemaService;
+
     @Test
     public void test_param_EventsCollectorRequestNotNull() throws IOException {
         doPOSTRequestTest(EVENT_COLLECTOR_URL, null, null, 400, ERROR_MESSAGE_INVALID_DATA_RECEIVED);
@@ -70,11 +78,25 @@ public class InputValidationIT extends BaseIT {
     }
 
     @Test
-    public void test_eventCollector_valid() throws IOException {
-        registerEventType(DUMMY_EVENT_TYPE_SCHEMA);
+    public void test_eventCollector_valid() throws IOException, InterruptedException {
+        // needed schema for event to be valid during tests
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json"));
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json"));
+        keepTrying("Event should be valid",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
         doPOSTRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_valid.json", 200, null);
         doGETRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_valid.json", 200, null);
-        unRegisterEventType("https://unomi.apache.org/schemas/json/events/dummy_event_type/1-0-0");
+
+        // remove schemas
+        schemaService.deleteSchema("https://unomi.apache.org/schemas/json/events/dummy/1-0-0");
+        schemaService.deleteSchema("https://unomi.apache.org/schemas/json/events/dummy/properties/1-0-0");
+        keepTrying("Event should be invalid",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> !isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
     }
 
     @Test
@@ -102,11 +124,25 @@ public class InputValidationIT extends BaseIT {
     }
 
     @Test
-    public void test_eventCollector_request_size_exceed_limit() throws IOException {
-        registerEventType(DUMMY_EVENT_TYPE_SCHEMA);
+    public void test_eventCollector_request_size_exceed_limit() throws IOException, InterruptedException {
+        // needed schema for event to be valid during tests
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json"));
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json"));
+        keepTrying("Event should be valid",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
         doPOSTRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_request_size_invalid.json", 400, ERROR_MESSAGE_REQUEST_SIZE_LIMIT_EXCEEDED);
         doPOSTRequestTest(EVENT_COLLECTOR_URL, null, "/validation/eventcollector_request_size_valid.json", 200, null);
-        unRegisterEventType("https://unomi.apache.org/schemas/json/events/dummy_event_type/1-0-0");
+
+        // remove schemas
+        schemaService.deleteSchema("https://unomi.apache.org/schemas/json/events/dummy/1-0-0");
+        schemaService.deleteSchema("https://unomi.apache.org/schemas/json/events/dummy/properties/1-0-0");
+        keepTrying("Event should be invalid",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> !isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
     }
 
     @Test
diff --git a/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java b/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java
index 97879bdd4..d323af407 100644
--- a/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/JSONSchemaIT.java
@@ -19,7 +19,7 @@ package org.apache.unomi.itests;
 
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.entity.ContentType;
-import org.apache.unomi.persistence.spi.PersistenceService;
+import org.apache.unomi.api.Event;
 import org.apache.unomi.schema.api.JsonSchemaWrapper;
 import org.apache.unomi.schema.api.SchemaService;
 import org.junit.After;
@@ -37,9 +37,7 @@ import java.util.Base64;
 import java.util.List;
 import java.util.Objects;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 
 /**
  * Class to tests the JSON schema features
@@ -56,10 +54,6 @@ public class JSONSchemaIT extends BaseIT {
     @Filter(timeout = 600000)
     protected SchemaService schemaService;
 
-    @Inject
-    @Filter(timeout = 600000)
-    protected PersistenceService persistenceService;
-
     @Before
     public void setUp() throws InterruptedException {
         keepTrying("Couldn't find json schema endpoint", () -> get(JSONSCHEMA_URL, List.class), Objects::nonNull, DEFAULT_TRYING_TIMEOUT,
@@ -67,76 +61,178 @@ public class JSONSchemaIT extends BaseIT {
     }
 
     @After
-    public void tearDown() {
-        schemaService.deleteSchema("https://unomi.apache.org/schemas/json/events/testEventType/1-0-0");
+    public void tearDown() throws InterruptedException {
+        removeItems(JsonSchemaWrapper.class, Event.class);
+        // ensure all schemas have been cleaned from schemaService.
+        keepTrying("Should not find json schemas anymore",
+                () -> schemaService.getInstalledJsonSchemaIds(),
+                (list) -> (!list.contains("https://unomi.apache.org/schemas/json/events/dummy/1-0-0") &&
+                        !list.contains("https://unomi.apache.org/schemas/json/events/dummy/properties/1-0-0")),
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
     }
 
     @Test
-    public void testGetJsonSchemasMetadatas() throws InterruptedException {
-        List jsonSchemas = get(JSONSCHEMA_URL, List.class);
-        assertTrue("JSON schema list should be empty", jsonSchemas.isEmpty());
+    public void testValidation_SaveDeleteSchemas() throws InterruptedException, IOException {
+        // check that event is not valid at first
+        assertFalse(schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"));
+
+        // Push schemas
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json"));
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json"));
+        keepTrying("Event should be valid",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
 
-        post(JSONSCHEMA_URL, "schemas/events/test-event-type.json", ContentType.TEXT_PLAIN);
+        // Test multiple invalid event:
+        // unevaluated property at root:
+        assertFalse(schemaService.isValid(resourceAsString("schemas/event-dummy-invalid-1.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"));
+        // unevaluated property in properties:
+        assertFalse(schemaService.isValid(resourceAsString("schemas/event-dummy-invalid-2.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"));
+        // bad type number but should be string:
+        assertFalse(schemaService.isValid(resourceAsString("schemas/event-dummy-invalid-3.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"));
+
+        // remove one of the schema:
+        assertTrue(schemaService.deleteSchema("https://unomi.apache.org/schemas/json/events/dummy/properties/1-0-0"));
+        keepTrying("Event should be invalid since of the schema have been deleted",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> !isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+    }
 
-        jsonSchemas = keepTrying("Couldn't find json schemas", () -> get(JSONSCHEMA_URL, List.class), (list) -> !list.isEmpty(),
+    @Test
+    public void testValidation_UpdateSchema() throws InterruptedException, IOException {
+        // check that event is not valid at first
+        assertFalse(schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"));
+
+        // Push schemas
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json"));
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json"));
+        keepTrying("Event should be valid",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+        // Test the invalid event, that use the new prop "invalidPropName" in properties:
+        assertFalse(schemaService.isValid(resourceAsString("schemas/event-dummy-invalid-2.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"));
+
+        // update the schema to allow "invalidPropName":
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties-updated.json"));
+        keepTrying("Event should be valid since of the schema have been updated",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-invalid-2.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> isValid,
                 DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
-        assertFalse("JSON schema list should not be empty", jsonSchemas.isEmpty());
-        assertEquals("JSON schema list should not be empty", 1, jsonSchemas.size());
     }
 
     @Test
-    public void testSaveNewValidJSONSchema() throws InterruptedException {
+    public void testExtension_SaveDelete() throws InterruptedException, IOException {
+        // Push base schemas
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json"));
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json"));
+        keepTrying("Event should be valid",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
 
-        assertTrue("JSON schema list should be empty", persistenceService.getAllItems(JsonSchemaWrapper.class).isEmpty());
+        // check that extended event is not valid at first
+        assertFalse(schemaService.isValid(resourceAsString("schemas/event-dummy-extended.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"));
 
-        CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/events/test-event-type.json", ContentType.TEXT_PLAIN);
+        // register both extensions (for root event and the properties level)
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-extension.json"));
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties-extension.json"));
+        keepTrying("Extended event should be valid since of the extensions have been deployed",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-extended.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
 
-        assertEquals("Invalid response code", 200, response.getStatusLine().getStatusCode());
-        List jsonSchemas = keepTrying("Couldn't find json schemas", () -> get(JSONSCHEMA_URL, List.class), (list) -> !list.isEmpty(),
+        // delete one of the extension
+        schemaService.deleteSchema("https://unomi.apache.org/schemas/json/events/dummy/properties/extension/1-0-0");
+        keepTrying("Extended event should be invalid again, one necessary extension have been removed",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-extended.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> !isValid,
                 DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
-        assertFalse("JSON schema list should not be empty", jsonSchemas.isEmpty());
     }
 
     @Test
-    public void testSavePredefinedJSONSchema() throws IOException {
-        assertTrue("JSON schema list should be empty", persistenceService.getAllItems(JsonSchemaWrapper.class).isEmpty());
-        try (CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/events/predefined-event-type.json", ContentType.TEXT_PLAIN)) {
-            assertEquals("Unable to save schema", 400, response.getStatusLine().getStatusCode());
-        }
+    public void testExtension_Update() throws InterruptedException, IOException {
+        // Push base schemas
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy.json"));
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties.json"));
+        keepTrying("Event should be valid",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-valid.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+        // check that extended event is not valid at first
+        assertFalse(schemaService.isValid(resourceAsString("schemas/event-dummy-extended.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"));
+
+        // register both extensions (for root event and the properties level)
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-extension.json"));
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties-extension.json"));
+        keepTrying("Extended event should be valid since of the extensions have been deployed",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-extended.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+
+        // check that extended event 2 is not valid due to usage of unevaluatedProperty not bring by schemas or extensions
+        assertFalse(schemaService.isValid(resourceAsString("schemas/event-dummy-extended-2.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"));
+
+        // Update extensions to allow the extended event 2
+        schemaService.saveSchema(resourceAsString("schemas/schema-dummy-properties-extension-2.json"));
+        keepTrying("Extended event 2 should be valid since of the extensions have been updated",
+                () -> schemaService.isValid(resourceAsString("schemas/event-dummy-extended-2.json"), "https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                isValid -> isValid,
+                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
     }
 
     @Test
-    public void testDeleteJSONSchema() throws InterruptedException {
-        assertTrue("JSON schema list should be empty", persistenceService.getAllItems(JsonSchemaWrapper.class).isEmpty());
+    public void testEndPoint_GetInstalledJsonSchemas() throws InterruptedException {
+        List<String> jsonSchemas = get(JSONSCHEMA_URL, List.class);
+        assertFalse("JSON schema list should not be empty, it should contain predefined Unomi schemas", jsonSchemas.isEmpty());
+    }
 
-        post(JSONSCHEMA_URL, "schemas/events/test-event-type.json", ContentType.TEXT_PLAIN);
+    @Test
+    public void testEndPoint_SaveDelete() throws InterruptedException, IOException {
+        assertNull(schemaService.getSchema("https://unomi.apache.org/schemas/json/events/dummy/1-0-0"));
 
-        keepTrying("Couldn't find json schemas", () -> get(JSONSCHEMA_URL, List.class), (list) -> !list.isEmpty(), DEFAULT_TRYING_TIMEOUT,
-                DEFAULT_TRYING_TRIES);
+        // Post schema using REST call
+        try(CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/schema-dummy.json", ContentType.TEXT_PLAIN)) {
+            assertEquals("Invalid response code", 200, response.getStatusLine().getStatusCode());
+        }
+
+        // See schema is available
+        keepTrying("Schema should have been created", () -> schemaService.getSchema("https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                Objects::nonNull, DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
 
+        // Delete Schema using REST call
         String encodedString = Base64.getEncoder()
-                .encodeToString("https://unomi.apache.org/schemas/json/events/testEventType/1-0-0".getBytes());
+                .encodeToString("https://unomi.apache.org/schemas/json/events/dummy/1-0-0".getBytes());
         CloseableHttpResponse response = delete(JSONSCHEMA_URL + "/" + encodedString);
         assertEquals("Invalid response code", 204, response.getStatusLine().getStatusCode());
 
-        List jsonSchemas = keepTrying("wait for empty list of schemas", () -> get(JSONSCHEMA_URL, List.class), List::isEmpty,
-                DEFAULT_TRYING_TIMEOUT, DEFAULT_TRYING_TRIES);
+        waitForNullValue("Schema should have been deleted",
+                () -> schemaService.getSchema("https://unomi.apache.org/schemas/json/events/dummy/1-0-0"),
+                DEFAULT_TRYING_TIMEOUT,
+                DEFAULT_TRYING_TRIES);
+    }
 
-        assertTrue("JSON schema list should be empty", jsonSchemas.isEmpty());
+    @Test
+    public void testSaveFail_PredefinedJSONSchema() throws IOException {
+        try (CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/schema-predefined.json", ContentType.TEXT_PLAIN)) {
+            assertEquals("Unable to save schema", 400, response.getStatusLine().getStatusCode());
+        }
     }
 
     @Test
-    public void testSaveNewInvalidJSONSchema() throws IOException {
-        assertTrue("JSON schema list should be empty", persistenceService.getAllItems(JsonSchemaWrapper.class).isEmpty());
-        try (CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/events/test-invalid.json", ContentType.TEXT_PLAIN)) {
+    public void testSaveFail_NewInvalidJSONSchema() throws IOException {
+        try (CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/schema-invalid.json", ContentType.TEXT_PLAIN)) {
             assertEquals("Unable to save schema", 400, response.getStatusLine().getStatusCode());
         }
     }
 
     @Test
-    public void testSaveSchemaWithInvalidName() throws IOException {
-        assertTrue("JSON schema list should be empty", persistenceService.getAllItems(JsonSchemaWrapper.class).isEmpty());
-        try (CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/events/test-invalid-name.json", ContentType.TEXT_PLAIN)) {
+    public void testSaveFail_SchemaWithInvalidName() throws IOException {
+        try (CloseableHttpResponse response = post(JSONSCHEMA_URL, "schemas/schema-invalid-name.json", ContentType.TEXT_PLAIN)) {
             assertEquals("Unable to save schema", 400, response.getStatusLine().getStatusCode());
         }
     }
diff --git a/itests/src/test/resources/schemas/event-dummy-extended-2.json b/itests/src/test/resources/schemas/event-dummy-extended-2.json
new file mode 100644
index 000000000..4014a1f6d
--- /dev/null
+++ b/itests/src/test/resources/schemas/event-dummy-extended-2.json
@@ -0,0 +1,11 @@
+{
+  "eventType":"dummy",
+  "scope":"dummy_scope",
+  "newRootProp": "extended Root Prop !",
+  "properties": {
+    "workspace": "dummy_workspace",
+    "path": "dummy/path",
+    "newProp": "extended Prop !",
+    "newProp2": 42
+  }
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/schemas/event-dummy-extended.json b/itests/src/test/resources/schemas/event-dummy-extended.json
new file mode 100644
index 000000000..6c6b9b60f
--- /dev/null
+++ b/itests/src/test/resources/schemas/event-dummy-extended.json
@@ -0,0 +1,10 @@
+{
+  "eventType":"dummy",
+  "scope":"dummy_scope",
+  "newRootProp": "extended Root Prop !",
+  "properties": {
+    "workspace": "dummy_workspace",
+    "path": "dummy/path",
+    "newProp": "extended Prop !"
+  }
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/schemas/event-dummy-invalid-1.json b/itests/src/test/resources/schemas/event-dummy-invalid-1.json
new file mode 100644
index 000000000..208c9edc2
--- /dev/null
+++ b/itests/src/test/resources/schemas/event-dummy-invalid-1.json
@@ -0,0 +1,9 @@
+{
+  "eventType":"dummy",
+  "scope":"dummy_scope",
+  "invalidPropName": "This is for sure an unknown property !!!!!",
+  "properties": {
+    "workspace": "dummy_workspace",
+    "path": "dummy/path"
+  }
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/schemas/event-dummy-invalid-2.json b/itests/src/test/resources/schemas/event-dummy-invalid-2.json
new file mode 100644
index 000000000..eb3be4cba
--- /dev/null
+++ b/itests/src/test/resources/schemas/event-dummy-invalid-2.json
@@ -0,0 +1,9 @@
+{
+  "eventType":"dummy",
+  "scope":"dummy_scope",
+  "properties": {
+    "invalidPropName": "This is for sure an unknown property !!!!!",
+    "workspace": "dummy_workspace",
+    "path": "dummy/path"
+  }
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/schemas/event-dummy-invalid-3.json b/itests/src/test/resources/schemas/event-dummy-invalid-3.json
new file mode 100644
index 000000000..841a598c9
--- /dev/null
+++ b/itests/src/test/resources/schemas/event-dummy-invalid-3.json
@@ -0,0 +1,8 @@
+{
+  "eventType":"dummy",
+  "scope":"dummy_scope",
+  "properties": {
+    "workspace": "dummy_workspace",
+    "path": 15
+  }
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/schemas/event-dummy-valid.json b/itests/src/test/resources/schemas/event-dummy-valid.json
new file mode 100644
index 000000000..ae55bdf33
--- /dev/null
+++ b/itests/src/test/resources/schemas/event-dummy-valid.json
@@ -0,0 +1,8 @@
+{
+  "eventType":"dummy",
+  "scope":"dummy_scope",
+  "properties": {
+    "workspace": "dummy_workspace",
+    "path": "dummy/path"
+  }
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/schemas/events/dummy-event-type.json b/itests/src/test/resources/schemas/events/dummy-event-type.json
deleted file mode 100644
index e17ce633f..000000000
--- a/itests/src/test/resources/schemas/events/dummy-event-type.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
-  "$id": "https://unomi.apache.org/schemas/json/events/dummy_event_type/1-0-0",
-  "$schema": "https://json-schema.org/draft/2019-09/schema",
-  "self": {
-    "vendor": "org.apache.unomi",
-    "name": "dummy_event_type",
-    "format": "jsonschema",
-    "target": "events",
-    "version": "1-0-0"
-  },
-  "title": "DummyEvent",
-  "type": "object",
-  "allOf": [
-    {
-      "$ref": "https://unomi.apache.org/schemas/json/event/1-0-0"
-    }
-  ],
-  "properties": {
-    "source": {
-      "type": "object",
-      "properties": {
-        "itemType": {
-          "type": "string"
-        },
-        "scope": {
-          "type": "string"
-        },
-        "itemId": {
-          "type": "string"
-        },
-        "properties": {
-          "type": "object",
-          "properties": {
-            "myProperty": {
-              "type": "string",
-              "maxLength": 20000
-            }
-          }
-        }
-      }
-    }
-  }
-}
diff --git a/itests/src/test/resources/schemas/events/negative-test-event-type.json b/itests/src/test/resources/schemas/events/negative-test-event-type.json
deleted file mode 100644
index 5508a5e99..000000000
--- a/itests/src/test/resources/schemas/events/negative-test-event-type.json
+++ /dev/null
@@ -1,13 +0,0 @@
-{
-  "$id": "https://unomi.apache.org/schemas/json/events/negativeTestEventType/1-0-0",
-  "$schema": "https://json-schema.org/draft/2019-09/schema",
-  "self":{
-    "vendor":"org.apache.unomi",
-    "name":"negativeTestEventType",
-    "format":"jsonschema",
-    "version":"1-0-0"
-  },
-  "title": "TestEvent",
-  "type": "object",
-  "allOf": [{ "$ref": "https://unomi.apache.org/schemas/json/event/1-0-0" }]
-}
diff --git a/itests/src/test/resources/schemas/schema-dummy-extension.json b/itests/src/test/resources/schemas/schema-dummy-extension.json
new file mode 100644
index 000000000..3c6f06d86
--- /dev/null
+++ b/itests/src/test/resources/schemas/schema-dummy-extension.json
@@ -0,0 +1,18 @@
+{
+  "$id": "https://unomi.apache.org/schemas/json/events/dummy/extension/1-0-0",
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "self":{
+    "vendor":"org.apache.unomi",
+    "name":"dummyExtension",
+    "format":"jsonschema",
+    "extends": "https://unomi.apache.org/schemas/json/events/dummy/1-0-0",
+    "version":"1-0-0"
+  },
+  "title": "DummyEventExtension",
+  "type": "object",
+  "properties": {
+    "newRootProp": {
+      "type": "string"
+    }
+  }
+}
diff --git a/itests/src/test/resources/schemas/schema-dummy-properties-extension-2.json b/itests/src/test/resources/schemas/schema-dummy-properties-extension-2.json
new file mode 100644
index 000000000..427a7f633
--- /dev/null
+++ b/itests/src/test/resources/schemas/schema-dummy-properties-extension-2.json
@@ -0,0 +1,21 @@
+{
+  "$id": "https://unomi.apache.org/schemas/json/events/dummy/properties/extension/1-0-0",
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "self": {
+    "vendor": "org.modules.jahia",
+    "name": "dummyPropertiesExtension",
+    "format": "jsonschema",
+    "version": "1-0-0",
+    "extends": "https://unomi.apache.org/schemas/json/events/dummy/properties/1-0-0"
+  },
+  "title": "dummyPropertiesExtension",
+  "type": "object",
+  "properties": {
+    "newProp": {
+      "type": "string"
+    },
+    "newProp2": {
+      "type": "number"
+    }
+  }
+}
diff --git a/itests/src/test/resources/schemas/schema-dummy-properties-extension.json b/itests/src/test/resources/schemas/schema-dummy-properties-extension.json
new file mode 100644
index 000000000..469cc7d29
--- /dev/null
+++ b/itests/src/test/resources/schemas/schema-dummy-properties-extension.json
@@ -0,0 +1,18 @@
+{
+  "$id": "https://unomi.apache.org/schemas/json/events/dummy/properties/extension/1-0-0",
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "self": {
+    "vendor": "org.modules.jahia",
+    "name": "dummyPropertiesExtension",
+    "format": "jsonschema",
+    "version": "1-0-0",
+    "extends": "https://unomi.apache.org/schemas/json/events/dummy/properties/1-0-0"
+  },
+  "title": "dummyPropertiesExtension",
+  "type": "object",
+  "properties": {
+    "newProp": {
+      "type": "string"
+    }
+  }
+}
diff --git a/itests/src/test/resources/schemas/schema-dummy-properties-updated.json b/itests/src/test/resources/schemas/schema-dummy-properties-updated.json
new file mode 100644
index 000000000..f92c2f9d2
--- /dev/null
+++ b/itests/src/test/resources/schemas/schema-dummy-properties-updated.json
@@ -0,0 +1,24 @@
+{
+  "$id": "https://unomi.apache.org/schemas/json/events/dummy/properties/1-0-0",
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "self": {
+    "vendor": "org.modules.jahia",
+    "name": "dummyProperties",
+    "format": "jsonschema",
+    "version": "1-0-0"
+  },
+  "title": "dummyProperties",
+  "type": "object",
+  "properties": {
+    "workspace": {
+      "type": "string"
+    },
+    "path": {
+      "type": "string"
+    },
+    "invalidPropName": {
+      "type": "string"
+    }
+  },
+  "unevaluatedProperties": false
+}
diff --git a/itests/src/test/resources/schemas/schema-dummy-properties.json b/itests/src/test/resources/schemas/schema-dummy-properties.json
new file mode 100644
index 000000000..f5cd6121d
--- /dev/null
+++ b/itests/src/test/resources/schemas/schema-dummy-properties.json
@@ -0,0 +1,22 @@
+{
+  "$id": "https://unomi.apache.org/schemas/json/events/dummy/properties/1-0-0",
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "self": {
+    "vendor": "org.modules.jahia",
+    "name": "dummyProperties",
+    "format": "jsonschema",
+    "version": "1-0-0"
+  },
+  "title": "dummyProperties",
+  "type": "object",
+  "properties": {
+    "workspace": {
+      "type": "string"
+    },
+    "path": {
+      "type": "string",
+      "maxLength": 20000
+    }
+  },
+  "unevaluatedProperties": false
+}
diff --git a/itests/src/test/resources/schemas/schema-dummy.json b/itests/src/test/resources/schemas/schema-dummy.json
new file mode 100644
index 000000000..6e133b25f
--- /dev/null
+++ b/itests/src/test/resources/schemas/schema-dummy.json
@@ -0,0 +1,22 @@
+{
+  "$id": "https://unomi.apache.org/schemas/json/events/dummy/1-0-0",
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "self":{
+    "vendor":"org.apache.unomi",
+    "name":"dummy",
+    "format":"jsonschema",
+    "target":"events",
+    "version":"1-0-0"
+  },
+  "title": "DummyEvent",
+  "type": "object",
+  "allOf": [
+    { "$ref": "https://unomi.apache.org/schemas/json/event/1-0-0" }
+  ],
+  "properties": {
+    "properties": {
+      "$ref": "https://unomi.apache.org/schemas/json/events/dummy/properties/1-0-0"
+    }
+  },
+  "unevaluatedProperties": false
+}
diff --git a/itests/src/test/resources/schemas/events/test-invalid-name.json b/itests/src/test/resources/schemas/schema-invalid-name.json
similarity index 100%
rename from itests/src/test/resources/schemas/events/test-invalid-name.json
rename to itests/src/test/resources/schemas/schema-invalid-name.json
diff --git a/itests/src/test/resources/schemas/events/test-invalid.json b/itests/src/test/resources/schemas/schema-invalid.json
similarity index 100%
rename from itests/src/test/resources/schemas/events/test-invalid.json
rename to itests/src/test/resources/schemas/schema-invalid.json
diff --git a/itests/src/test/resources/schemas/events/predefined-event-type.json b/itests/src/test/resources/schemas/schema-predefined.json
similarity index 79%
rename from itests/src/test/resources/schemas/events/predefined-event-type.json
rename to itests/src/test/resources/schemas/schema-predefined.json
index 8d9ffd967..b4da0ada6 100644
--- a/itests/src/test/resources/schemas/events/predefined-event-type.json
+++ b/itests/src/test/resources/schemas/schema-predefined.json
@@ -10,5 +10,7 @@
   },
   "title": "TestEvent",
   "type": "object",
-  "allOf": [{ "$ref": "https://unomi.apache.org/schemas/json/event/1-0-0" }]
+  "allOf": [
+    { "$ref": "https://unomi.apache.org/schemas/json/event/1-0-0" }
+  ]
 }
diff --git a/itests/src/test/resources/validation/eventcollector_invalidSessionId.json b/itests/src/test/resources/validation/eventcollector_invalidSessionId.json
index bdea1634b..4a35cdc78 100644
--- a/itests/src/test/resources/validation/eventcollector_invalidSessionId.json
+++ b/itests/src/test/resources/validation/eventcollector_invalidSessionId.json
@@ -2,15 +2,11 @@
   "sessionId": "<script>alert();</script>",
   "events":[
     {
-      "eventType":"dummy_event_type",
+      "eventType":"dummy",
       "scope":"dummy_scope",
-      "source":{
-        "itemType":"pageView",
-        "scope":"dummy_scope",
-        "itemId":"pageView",
-        "properties":{
-          "myProperty":"myValue"
-        }
+      "properties": {
+        "workspace": "dummy_workspace",
+        "path": "dummy/path"
       }
     }
   ]
diff --git a/itests/src/test/resources/validation/eventcollector_request_size_invalid.json b/itests/src/test/resources/validation/eventcollector_request_size_invalid.json
index 9813ba3bc..a6b269fbe 100644
--- a/itests/src/test/resources/validation/eventcollector_request_size_invalid.json
+++ b/itests/src/test/resources/validation/eventcollector_request_size_invalid.json
@@ -2,15 +2,11 @@
   "sessionId": "dummy-session-id",
   "events":[
     {
-      "eventType":"dummy_event_type",
+      "eventType":"dummy",
       "scope":"dummy_scope",
-      "source":{
-        "itemType":"pageView",
-        "scope":"dummy_scope",
-        "itemId":"pageView",
-        "properties":{
-          "myProperty":"Where doesWhere does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. Ithere does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. It here does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. It here does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. It here does it come from?\nContrary to popular belief, Lorem Ipsum is not simply [...]
-        }
+      "properties": {
+        "workspace": "dummy_workspace",
+        "path": "Where doesWhere does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. Ithere does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. It here does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. It here does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random text. It here does it come from?\nContrary to popular belief, Lorem Ipsum is not simply random [...]
       }
     }
   ]
diff --git a/itests/src/test/resources/validation/eventcollector_request_size_valid.json b/itests/src/test/resources/validation/eventcollector_request_size_valid.json
index 6fc32fa1d..329a3c4b1 100644
--- a/itests/src/test/resources/validation/eventcollector_request_size_valid.json
+++ b/itests/src/test/resources/validation/eventcollector_request_size_valid.json
@@ -2,15 +2,11 @@
   "sessionId": "dummy-session-id",
   "events":[
     {
-      "eventType":"dummy_event_type",
+      "eventType":"dummy",
       "scope":"dummy_scope",
-      "source":{
-        "itemType":"pageView",
-        "scope":"dummy_scope",
-        "itemId":"pageView",
-        "properties":{
-          "myProperty": "Well sized property"
-        }
+      "properties": {
+        "workspace": "dummy_workspace",
+        "path": "dummy/path/well/sized"
       }
     }
   ]
diff --git a/itests/src/test/resources/validation/eventcollector_valid.json b/itests/src/test/resources/validation/eventcollector_valid.json
index 29a39cf96..edc63fe78 100644
--- a/itests/src/test/resources/validation/eventcollector_valid.json
+++ b/itests/src/test/resources/validation/eventcollector_valid.json
@@ -2,15 +2,11 @@
   "sessionId": "dummy-session-id",
   "events":[
     {
-      "eventType":"dummy_event_type",
+      "eventType":"dummy",
       "scope":"dummy_scope",
-      "source":{
-        "itemType":"pageView",
-        "scope":"dummy_scope",
-        "itemId":"pageView",
-        "properties":{
-          "myProperty":"myValue"
-        }
+      "properties": {
+        "workspace": "dummy_workspace",
+        "path": "dummy/path"
       }
     }
   ]
diff --git a/kar/pom.xml b/kar/pom.xml
index 23824b273..67335e982 100644
--- a/kar/pom.xml
+++ b/kar/pom.xml
@@ -119,6 +119,11 @@
             <artifactId>unomi-json-schema-services</artifactId>
             <version>${project.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.unomi</groupId>
+            <artifactId>unomi-json-schema-rest</artifactId>
+            <version>${project.version}</version>
+        </dependency>
         <dependency>
             <groupId>org.apache.unomi</groupId>
             <artifactId>unomi-groovy-actions-services</artifactId>