Repository: epam/ai-dial-core
Pull request #801: feat: Special flag for validation handling to list or get schema-rich applications
Author: login: sergey-zinchenko
Labels: name: documentation
description: Improvements or additions to documentation
Created: 2025-04-25T21:41:44Z | Merged: 2025-04-26T12:19:53Z

### Applicable issues

Currently, if the application does not correspond to its schema, a server error will be returned while listing applications.

### Description of changes

An additional `is_invalid_application` flag is introduced to mark such applications, and `application_properties` will be returned as null.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.



Diff:
diff --git a/config/src/main/java/com/epam/aidial/core/config/Application.java b/config/src/main/java/com/epam/aidial/core/config/Application.java
index 9228ed17d..2ed1bf057 100644
--- a/config/src/main/java/com/epam/aidial/core/config/Application.java
+++ b/config/src/main/java/com/epam/aidial/core/config/Application.java
@@ -3,6 +3,7 @@
 import com.fasterxml.jackson.annotation.JsonAlias;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import com.fasterxml.jackson.databind.PropertyNamingStrategies;
 import com.fasterxml.jackson.databind.annotation.JsonNaming;
 import lombok.Data;
@@ -33,6 +34,16 @@ public Boolean hasApplicationTypeSchemaId() {
         return applicationTypeSchemaId != null;
     }
 
+    /**
+     * Indicates whether the application is invalid.
+     * Only applicable for schema-rich applications.
+     * Set to true when validation fails (e.g., missing required properties or schema violations).
+     * Null when the application is valid.
+     * This is a read-only property (not parsed from JSON).
+     */
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    private Boolean invalid = null;
+
     @Data
     @Accessors(chain = true)
     @JsonInclude(JsonInclude.Include.NON_NULL)
@@ -111,6 +122,7 @@ public Application() {
 
     public Application(Application source) {
         super();
+        this.setInvalid(source.getInvalid());
         this.setName(source.getName());
         this.setEndpoint(source.getEndpoint());
         this.setDisplayName(source.getDisplayName());
diff --git a/server/src/main/java/com/epam/aidial/core/server/AiDial.java b/server/src/main/java/com/epam/aidial/core/server/AiDial.java
index 7c2701d7c..afd84662b 100644
--- a/server/src/main/java/com/epam/aidial/core/server/AiDial.java
+++ b/server/src/main/java/com/epam/aidial/core/server/AiDial.java
@@ -89,6 +89,7 @@ public class AiDial {
 
     private BlobStorage storage;
     private ResourceService resourceService;
+    private EncryptionService encryptionService;
 
     private LongSupplier clock = System::currentTimeMillis;
     private Supplier<String> generator = () -> UUID.randomUUID().toString().replace("-", "");
@@ -116,7 +117,7 @@ void start() throws Exception {
                 Storage storageConfig = Json.decodeValue(settings("storage").toBuffer(), Storage.class);
                 storage = new BlobStorage(storageConfig);
             }
-            EncryptionService encryptionService = new EncryptionService(settings("encryption"));
+            encryptionService = new EncryptionService(settings("encryption"));
 
             redis = CacheClientFactory.create(toJsonNode(settings("redis")));
 
diff --git a/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java b/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java
index 61caaf293..b6ed48e99 100644
--- a/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java
+++ b/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java
@@ -13,9 +13,12 @@
 import com.epam.aidial.core.server.service.DeploymentService;
 import com.epam.aidial.core.server.service.PermissionDeniedException;
 import com.epam.aidial.core.server.service.ResourceNotFoundException;
+import com.epam.aidial.core.server.util.ApplicationTypeSchemaProcessingException;
 import com.epam.aidial.core.server.util.ApplicationTypeSchemaUtils;
 import com.epam.aidial.core.server.util.ProxyUtil;
 import com.epam.aidial.core.server.util.ResourceDescriptorFactory;
+import com.epam.aidial.core.server.validation.ApplicationTypeResourceException;
+import com.epam.aidial.core.server.validation.ApplicationTypeSchemaValidationException;
 import com.epam.aidial.core.storage.http.HttpException;
 import com.epam.aidial.core.storage.http.HttpStatus;
 import com.epam.aidial.core.storage.resource.ResourceDescriptor;
@@ -28,6 +31,8 @@
 import java.util.List;
 import java.util.Objects;
 
+import static com.epam.aidial.core.server.util.ApplicationTypeSchemaUtils.modifySchemaRichApplication;
+
 @Slf4j
 public class ApplicationController {
 
@@ -49,15 +54,11 @@ public ApplicationController(ProxyContext context) {
     }
 
     public Future<?> getApplication(String applicationId) {
-        boolean propertyFilteringRequired = !applicationId.equals(context.getDecodedSourceDeployment());
         vertx.executeBlocking(() -> deploymentService.findDeployment(context, applicationId), false)
                 .map(deployment -> {
                     if (deployment instanceof Application application) {
-                        if (propertyFilteringRequired) {
-                            application = ApplicationTypeSchemaUtils.filterCustomClientProperties(context.getConfig(), application);
-                        }
-                        application = ApplicationTypeSchemaUtils.modifyEndpointsForCustomApplication(context.getConfig(), application);
-                        return application;
+                        boolean applicationRequestInfoAboutItSelf = applicationId.equals(context.getDecodedSourceDeployment());
+                        return modifySchemaRichApplication(application, !applicationRequestInfoAboutItSelf, context);
                     }
                     throw new ResourceNotFoundException("Application is not found: " + applicationId);
                 })
@@ -68,6 +69,8 @@ public Future<?> getApplication(String applicationId) {
         return Future.succeededFuture();
     }
 
+
+
     public Future<?> getApplications() {
         Config config = context.getConfig();
         Proxy proxy = context.getProxy();
@@ -77,10 +80,7 @@ public Future<?> getApplications() {
             for (Application application : config.getApplications().values()) {
                 if (application.hasAccess(context.getUserRoles())) {
                     boolean applicationRequestInfoAboutItSelf = Objects.equals(context.getDecodedSourceDeployment(), UrlUtil.decodePath(application.getName()));
-                    if (!applicationRequestInfoAboutItSelf) {
-                        application = ApplicationTypeSchemaUtils.filterCustomClientProperties(config, application);
-                    }
-                    application = ApplicationTypeSchemaUtils.modifyEndpointsForCustomApplication(config, application);
+                    application = modifySchemaRichApplication(application, !applicationRequestInfoAboutItSelf, context);
                     list.add(application);
                 }
             }
diff --git a/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationUtil.java b/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationUtil.java
index d4edb004d..293d43404 100644
--- a/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationUtil.java
+++ b/server/src/main/java/com/epam/aidial/core/server/controller/ApplicationUtil.java
@@ -13,6 +13,7 @@ public class ApplicationUtil {
 
     public ApplicationData mapApplication(Application application) {
         ApplicationData data = new ApplicationData();
+        data.setInvalid(application.getInvalid());
         data.setId(application.getName());
         data.setApplication(application.getName());
         data.setDisplayName(application.getDisplayName());
diff --git a/server/src/main/java/com/epam/aidial/core/server/controller/ResourceController.java b/server/src/main/java/com/epam/aidial/core/server/controller/ResourceController.java
index e4168b185..8df02c7be 100644
--- a/server/src/main/java/com/epam/aidial/core/server/controller/ResourceController.java
+++ b/server/src/main/java/com/epam/aidial/core/server/controller/ResourceController.java
@@ -164,7 +164,14 @@ private Application clearApplicationProperties(Application application, Config c
             features.setTokenizeEndpoint(null);
             features.setTruncatePromptEndpoint(null);
         }
-        return ApplicationTypeSchemaUtils.filterCustomClientProperties(config, application);
+        try {
+            return ApplicationTypeSchemaUtils.filterCustomClientProperties(config, application);
+        } catch (ApplicationTypeSchemaProcessingException | ApplicationTypeResourceException | ApplicationTypeSchemaValidationException ex) {
+            log.error("Failed to modify application to fulfill schema's restrictions %s".formatted(application.getName()), ex);
+            application.setApplicationProperties(null);
+            application.setInvalid(true);
+            return application;
+        }
     }
 
     private Future<Pair<ResourceItemMetadata, String>> getResourceData(ResourceDescriptor descriptor, EtagHeader etag) {
diff --git a/server/src/main/java/com/epam/aidial/core/server/data/ApplicationData.java b/server/src/main/java/com/epam/aidial/core/server/data/ApplicationData.java
index 34918c681..68b78ecd3 100644
--- a/server/src/main/java/com/epam/aidial/core/server/data/ApplicationData.java
+++ b/server/src/main/java/com/epam/aidial/core/server/data/ApplicationData.java
@@ -22,6 +22,16 @@ public class ApplicationData extends DeploymentData {
         setScaleSettings(null);
     }
 
+    /**
+     * Indicates whether the application is invalid.
+     * Only applicable for schema-rich applications.
+     * Set to true when validation fails (e.g., missing required properties or schema violations).
+     * Null when the application is valid.
+     * This is a read-only property (not parsed from JSON).
+     */
+    @Nullable
+    public Boolean invalid = null;
+
     @Nullable
     private Map<String, Object> applicationProperties; //all custom application properties will land there
 
diff --git a/server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java b/server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java
index 26c023c72..a41dd133f 100644
--- a/server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java
+++ b/server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java
@@ -12,10 +12,13 @@
 import com.epam.aidial.core.server.security.AccessService;
 import com.epam.aidial.core.server.security.ApiKeyStore;
 import com.epam.aidial.core.server.security.EncryptionService;
+import com.epam.aidial.core.server.util.ApplicationTypeSchemaProcessingException;
 import com.epam.aidial.core.server.util.ApplicationTypeSchemaUtils;
 import com.epam.aidial.core.server.util.BucketBuilder;
 import com.epam.aidial.core.server.util.ProxyUtil;
 import com.epam.aidial.core.server.util.ResourceDescriptorFactory;
+import com.epam.aidial.core.server.validation.ApplicationTypeResourceException;
+import com.epam.aidial.core.server.validation.ApplicationTypeSchemaValidationException;
 import com.epam.aidial.core.storage.blobstore.BlobStorageUtil;
 import com.epam.aidial.core.storage.data.MetadataBase;
 import com.epam.aidial.core.storage.data.NodeType;
@@ -131,12 +134,7 @@ public List<Application> getSharedApplications(ProxyContext context) {
 
             if (meta instanceof ResourceItemMetadata) {
                 try {
-                    Application application = getApplication(resource).getValue();
-                    boolean applicationRequestInfoAboutItSelf = Objects.equals(context.getDecodedSourceDeployment(),
-                            resource.getDecodedUrl());
-                    if (!applicationRequestInfoAboutItSelf) {
-                        application = ApplicationTypeSchemaUtils.filterCustomClientPropertiesWhenNoWriteAccess(context, resource, application);
-                    }
+                    Application application = extractApplicationFromResource(resource, resource, context, meta);
                     list.add(application);
                 } catch (ResourceNotFoundException ignore) {
                     // skip shared app which might be deleted incidentally
@@ -209,13 +207,7 @@ public List<Application> getApplications(ResourceDescriptor resource,
                 if (meta.getNodeType() == NodeType.ITEM && meta.getResourceType() == ResourceTypes.APPLICATION) {
                     try {
                         ResourceDescriptor item = ResourceDescriptorFactory.fromAnyUrl(meta.getUrl(), encryptionService);
-                        Application application = getApplication(item).getValue();
-                        boolean applicationRequestInfoAboutItSelf = !Objects.equals(ctx.getDecodedSourceDeployment(),
-                                resource.getDecodedUrl());
-                        if (applicationRequestInfoAboutItSelf) {
-                            application = ApplicationTypeSchemaUtils.filterCustomClientPropertiesWhenNoWriteAccess(ctx, item, application);
-                        }
-                        application = ApplicationTypeSchemaUtils.modifyEndpointsForCustomApplication(ctx.getConfig(), application);
+                        Application application = extractApplicationFromResource(resource, item, ctx, meta);
                         applications.add(application);
                     } catch (ResourceNotFoundException ignore) {
                         // deleted while fetching
@@ -229,6 +221,27 @@ public List<Application> getApplications(ResourceDescriptor resource,
         return applications;
     }
 
+    private Application extractApplicationFromResource(ResourceDescriptor resource, ResourceDescriptor item, ProxyContext ctx, MetadataBase meta) {
+        Application application = getApplication(item).getValue();
+        return modifySchemRichApplication(resource, ctx, application, item);
+    }
+
+    private static Application modifySchemRichApplication(ResourceDescriptor resource, ProxyContext ctx, Application application, ResourceDescriptor item) {
+        try {
+            boolean applicationRequestInfoAboutItSelf = !Objects.equals(ctx.getDecodedSourceDeployment(),
+                    resource.getDecodedUrl());
+            if (applicationRequestInfoAboutItSelf) {
+                application = ApplicationTypeSchemaUtils.filterCustomClientPropertiesWhenNoWriteAccess(ctx, item, application);
+            }
+            application = ApplicationTypeSchemaUtils.modifyEndpointsForCustomApplication(ctx.getConfig(), application);
+        } catch (ApplicationTypeSchemaProcessingException | ApplicationTypeResourceException | ApplicationTypeSchemaValidationException ex) {
+            log.error("Failed to modify application to fulfill schema's restrictions %s".formatted(application.getName()), ex);
+            application.setApplicationProperties(null);
+            application.setInvalid(true);
+        }
+        return application;
+    }
+
     public Pair<ResourceItemMetadata, Application> putApplication(ResourceDescriptor resource, EtagHeader etag, String author, Application application) {
         prepareApplication(resource, application);
 
diff --git a/server/src/main/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtils.java b/server/src/main/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtils.java
index 7c580f9d5..ebdccd776 100644
--- a/server/src/main/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtils.java
+++ b/server/src/main/java/com/epam/aidial/core/server/util/ApplicationTypeSchemaUtils.java
@@ -25,6 +25,7 @@
 import com.networknt.schema.JsonSchemaFactory;
 import com.networknt.schema.ValidationMessage;
 import lombok.experimental.UtilityClass;
+import lombok.extern.slf4j.Slf4j;
 
 import java.net.URI;
 import java.util.ArrayList;
@@ -41,7 +42,7 @@
 import static com.epam.aidial.core.metaschemas.MetaSchemaHolder.APPLICATION_TYPE_TRUNCATE_PROMPT_ENDPOINT;
 import static com.epam.aidial.core.metaschemas.MetaSchemaHolder.getMetaschemaBuilder;
 
-
+@Slf4j
 @UtilityClass
 public class ApplicationTypeSchemaUtils {
 
@@ -293,4 +294,18 @@ public static void replaceLinksInJsonNode(JsonNode node, Map<String, String> rep
             }
         }
     }
+
+    public static Application modifySchemaRichApplication(Application application, boolean propertyFilteringRequired, ProxyContext context) {
+        try {
+            if (propertyFilteringRequired) {
+                application = ApplicationTypeSchemaUtils.filterCustomClientProperties(context.getConfig(), application);
+            }
+            application = ApplicationTypeSchemaUtils.modifyEndpointsForCustomApplication(context.getConfig(), application);
+        } catch (ApplicationTypeSchemaProcessingException | ApplicationTypeResourceException | ApplicationTypeSchemaValidationException ex) {
+            log.error("Failed to modify application to fulfill schema's restrictions %s".formatted(application.getName()), ex);
+            application.setApplicationProperties(null);
+            application.setInvalid(true);
+        }
+        return application;
+    }
 }
\ No newline at end of file
diff --git a/server/src/test/java/com/epam/aidial/core/server/CustomApplicationApiTest.java b/server/src/test/java/com/epam/aidial/core/server/CustomApplicationApiTest.java
index 5037ffd3b..ab9458ac4 100644
--- a/server/src/test/java/com/epam/aidial/core/server/CustomApplicationApiTest.java
+++ b/server/src/test/java/com/epam/aidial/core/server/CustomApplicationApiTest.java
@@ -3,6 +3,9 @@
 import com.epam.aidial.core.config.Application;
 import com.epam.aidial.core.server.data.InvitationLink;
 import com.epam.aidial.core.server.util.ProxyUtil;
+import com.epam.aidial.core.server.util.ResourceDescriptorFactory;
+import com.epam.aidial.core.storage.resource.ResourceDescriptor;
+import com.epam.aidial.core.storage.util.EtagHeader;
 import io.vertx.core.http.HttpMethod;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -1246,4 +1249,154 @@ void testApplicationWithTypeSchemaCreationAndThenUpdate_Ok_WhenApplicationProper
         Assertions.assertEquals(200, response.status());
     }
 
+    @Test
+    void testOpenAiApplicationWithTypeSchemaGet_ReturnedInvalid_WhenAppDoesNotConformToSchema() {
+        //create valid app
+        Response response = upload(HttpMethod.PUT, "/v1/files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_file1.txt", null, """
+                  Test1
+                """);
+
+        Assertions.assertEquals(200, response.status());
+
+        response = upload(HttpMethod.PUT, "/v1/files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_file2.txt", null, """
+                  Test2
+                """);
+
+        Assertions.assertEquals(200, response.status());
+
+        response = send(HttpMethod.PUT, "/v1/applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_app_to_fail", null, """
+                  {
+                      "displayName": "test_app",
+                      "applicationTypeSchemaId": "https://mydial.somewhere.com/custom_application_schemas/specific_application_type",
+                       "applicationProperties": {
+                        "property1": "test property1",
+                        "property2": "test property2",
+                        "property3": [
+                                "files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_file1.txt",
+                                "files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_file2.txt"
+                        ]
+                       },
+                       "userRoles": [
+                            "Admin"
+                       ],
+                       "forwardAuthToken": true,
+                       "iconUrl": "https://mydial.somewhere.com/app-icon.svg",
+                       "description": "My application description"
+                  }
+                """);
+        Assertions.assertEquals(200, response.status());
+
+        //share valid app
+        response = operationRequest("/v1/ops/resource/share/create", """
+                {
+                  "invitationType": "link",
+                  "resources": [
+                    {
+                      "url": "applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_app_to_fail",
+                      "permissions": [ "READ" ]
+                    }
+                  ]
+                }
+                """);
+        verify(response, 200);
+        InvitationLink invitationLink = ProxyUtil.convertToObject(response.body(), InvitationLink.class);
+        assertNotNull(invitationLink);
+
+        response = send(HttpMethod.GET, invitationLink.invitationLink(), "accept=true", null, "Api-key", "proxyKey2");
+        verify(response, 200);
+
+        //broke the app
+        ResourceDescriptor descriptor =
+                ResourceDescriptorFactory.fromAnyUrl("applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_app_to_fail", this.dial.getEncryptionService());
+
+        this.dial.getResourceService().putResource(descriptor, """
+                  {
+                      "displayName": "test_app",
+                      "applicationTypeSchemaId": "https://mydial.somewhere.com/custom_application_schemas/specific_application_type",
+                       "applicationProperties": {},
+                       "userRoles": [
+                            "Admin"
+                       ],
+                       "iconUrl": "https://mydial.somewhere.com/app-icon.svg",
+                       "description": "My application description"
+                  }
+                """, EtagHeader.ANY);
+
+        //making get request from other user and check invalid flag
+
+        response = send(HttpMethod.GET, "/openai/applications", null, null, "Api-key", "proxyKey2");
+
+        verifyJsonNotExact(response, 200, """
+                  {
+                     "data" : [ {
+                     "id" : "app",
+                     "application" : "app",
+                     "display_name" : "10k",
+                     "icon_url" : "http://localhost:7001/logo10k.png",
+                     "description" : "Some description of the application for testing",
+                     "reference" : "app",
+                     "owner" : "organization-owner",
+                     "object" : "application",
+                     "status" : "succeeded",
+                     "created_at" : "@ignore",
+                     "updated_at" : "@ignore",
+                     "features" : {
+                       "rate" : true,
+                       "tokenize" : false,
+                       "truncate_prompt" : false,
+                       "configuration" : true,
+                       "system_prompt" : false,
+                       "tools" : false,
+                       "seed" : false,
+                       "url_attachments" : false,
+                       "folder_attachments" : false,
+                       "allow_resume" : true,
+                       "accessible_by_per_request_key" : true,
+                       "content_parts" : false,
+                       "temperature" : true,
+                       "addons" : true,
+                       "cache" : false,
+                       "auto_caching" : false
+                     },
+                     "defaults" : { },
+                     "description_keywords" : [ ],
+                     "max_retry_attempts" : 1
+                   }, {
+                     "display_name" : "test_app",
+                     "icon_url" : "https://mydial.somewhere.com/app-icon.svg",
+                     "description" : "My application description",
+                     "owner" : "EPM-RTC-GPT",
+                     "object" : "application",
+                     "status" : "succeeded",
+                     "created_at" : "@ignore",
+                     "updated_at" : "@ignore",
+                     "features" : {
+                       "rate" : false,
+                       "tokenize" : false,
+                       "truncate_prompt" : false,
+                       "configuration" : false,
+                       "system_prompt" : true,
+                       "tools" : false,
+                       "seed" : false,
+                       "url_attachments" : false,
+                       "folder_attachments" : false,
+                       "allow_resume" : true,
+                       "accessible_by_per_request_key" : true,
+                       "content_parts" : false,
+                       "temperature" : true,
+                       "addons" : true,
+                       "cache" : false,
+                       "auto_caching" : false
+                     },
+                     "defaults" : { },
+                     "description_keywords" : [ ],
+                     "max_retry_attempts" : 1,
+                     "invalid" : true,
+                     "application_type_schema_id" : "https://mydial.somewhere.com/custom_application_schemas/specific_application_type"
+                   } ],
+                   "object" : "list"
+                }
+                """);
+    }
+
 }
diff --git a/server/src/test/java/com/epam/aidial/core/server/ResourceApiTest.java b/server/src/test/java/com/epam/aidial/core/server/ResourceApiTest.java
index 4921d4056..454fbddc3 100644
--- a/server/src/test/java/com/epam/aidial/core/server/ResourceApiTest.java
+++ b/server/src/test/java/com/epam/aidial/core/server/ResourceApiTest.java
@@ -1,5 +1,10 @@
 package com.epam.aidial.core.server;
 
+import com.epam.aidial.core.server.data.InvitationLink;
+import com.epam.aidial.core.server.util.ProxyUtil;
+import com.epam.aidial.core.server.util.ResourceDescriptorFactory;
+import com.epam.aidial.core.storage.resource.ResourceDescriptor;
+import com.epam.aidial.core.storage.util.EtagHeader;
 import io.vertx.core.http.HttpMethod;
 import lombok.extern.slf4j.Slf4j;
 import org.junit.jupiter.api.Assertions;
@@ -9,6 +14,7 @@
 import java.util.concurrent.TimeUnit;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @Slf4j
@@ -423,4 +429,102 @@ void testApplicationWithTypeSchemaCreation_Failed_FailMissingProps() {
                 """);
         Assertions.assertEquals(400, response.status());
     }
+
+    @Test
+    void testApplicationWithTypeSchemaGet_ReturnedInvalid_WhenAppDoesNotConformToSchema() {
+        //create valid app
+        Response response = upload(HttpMethod.PUT, "/v1/files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_file1.txt", null, """
+                  Test1
+                """);
+
+        Assertions.assertEquals(200, response.status());
+
+        response = upload(HttpMethod.PUT, "/v1/files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_file2.txt", null, """
+                  Test2
+                """);
+
+        Assertions.assertEquals(200, response.status());
+
+        response = send(HttpMethod.PUT, "/v1/applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_app_to_fail", null, """
+                  {
+                      "displayName": "test_app",
+                      "applicationTypeSchemaId": "https://mydial.somewhere.com/custom_application_schemas/specific_application_type",
+                       "applicationProperties": {
+                        "property1": "test property1",
+                        "property2": "test property2",
+                        "property3": [
+                                "files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_file1.txt",
+                                "files/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_file2.txt"
+                        ]
+                       },
+                       "userRoles": [
+                            "Admin"
+                       ],
+                       "forwardAuthToken": true,
+                       "iconUrl": "https://mydial.somewhere.com/app-icon.svg",
+                       "description": "My application description"
+                  }
+                """);
+        Assertions.assertEquals(200, response.status());
+
+        //share valid app
+        response = operationRequest("/v1/ops/resource/share/create", """
+                {
+                  "invitationType": "link",
+                  "resources": [
+                    {
+                      "url": "applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_app_to_fail",
+                      "permissions": [ "READ" ]
+                    }
+                  ]
+                }
+                """);
+        verify(response, 200);
+        InvitationLink invitationLink = ProxyUtil.convertToObject(response.body(), InvitationLink.class);
+        assertNotNull(invitationLink);
+
+        response = send(HttpMethod.GET, invitationLink.invitationLink(), "accept=true", null, "Api-key", "proxyKey2");
+        verify(response, 200);
+
+        //broke the app
+        ResourceDescriptor descriptor =
+                ResourceDescriptorFactory.fromAnyUrl("applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_app_to_fail", this.dial.getEncryptionService());
+
+        this.dial.getResourceService().putResource(descriptor, """
+                  {
+                      "displayName": "test_app",
+                      "applicationTypeSchemaId": "https://mydial.somewhere.com/custom_application_schemas/specific_application_type",
+                       "applicationProperties": {},
+                       "userRoles": [
+                            "Admin"
+                       ],
+                       "iconUrl": "https://mydial.somewhere.com/app-icon.svg",
+                       "description": "My application description"
+                  }
+                """, EtagHeader.ANY);
+
+        //making get request from other user and check invalid flag
+
+        response = send(HttpMethod.GET, "/v1/applications/3CcedGxCx23EwiVbVmscVktScRyf46KypuBQ65miviST/test_app_to_fail", null, null, "Api-key", "proxyKey2");
+
+        verifyJsonNotExact(response, 200, """
+                {
+                    "user_roles" : [ "Admin" ],
+                    "display_name" : "test_app",
+                    "icon_url" : "https://mydial.somewhere.com/app-icon.svg",
+                    "description" : "My application description",
+                    "forward_auth_token" : false,
+                    "defaults" : { },
+                    "interceptors" : [ ],
+                    "description_keywords" : [ ],
+                    "max_retry_attempts" : 1,
+                    "author" : "EPM-RTC-GPT",
+                    "created_at" : "@ignore",
+                    "updated_at" : "@ignore",
+                    "dependencies" : [ ],
+                    "application_type_schema_id" : "https://mydial.somewhere.com/custom_application_schemas/specific_application_type",
+                    "invalid" : true
+                }
+                """);
+    }
 }
\ No newline at end of file


Reviews:
user: login: astsiapanay
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-26T06:45:54Z
user: login: sergey-zinchenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T07:51:53Z
user: login: sergey-zinchenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T07:52:57Z
user: login: sergey-zinchenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T08:11:29Z
user: login: sergey-zinchenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T08:18:00Z
user: login: sergey-zinchenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T09:27:51Z
user: login: sergey-zinchenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T10:13:52Z
user: login: astsiapanay
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-26T11:32:01Z
user: login: sergey-zinchenko
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T11:35:00Z
user: login: astsiapanay
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-26T11:42:36Z

Review comments:
diff_hunk: @@ -33,6 +34,9 @@ public Boolean hasApplicationTypeSchemaId() {
         return applicationTypeSchemaId != null;
     }
 
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    private Boolean isInvalidApplication = null;
path: config/src/main/java/com/epam/aidial/core/config/Application.java
user: login: astsiapanay
body: Perhaps we want to make the name shorter like `invalid`?
Please put a comment explaining what the flag means and where it's used to
created_at: 2025-04-26T06:39:29Z
author_association: COLLABORATOR
diff_hunk: @@ -68,6 +67,20 @@ public Future<?> getApplication(String applicationId) {
         return Future.succeededFuture();
     }
 
+    private Application modifySchemaRichApplication(Application application, boolean propertyFilteringRequired) {
path: server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java
user: login: astsiapanay
body: I suppose the method is a good candidate to move it to the app util class.
created_at: 2025-04-26T06:40:31Z
author_association: COLLABORATOR
diff_hunk: @@ -671,8 +684,8 @@ private String deploymentLockKey(ResourceDescriptor resource) {
 
     private String encodeTargetFolder(ResourceDescriptor resource, String id) {
         String location = resource.getBucketLocation()
-                + DEPLOYMENTS_NAME + ResourceDescriptor.PATH_SEPARATOR
-                + id + ResourceDescriptor.PATH_SEPARATOR;
+                          + DEPLOYMENTS_NAME + ResourceDescriptor.PATH_SEPARATOR
path: server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java
user: login: astsiapanay
body: please remove the re-formating
created_at: 2025-04-26T06:42:31Z
author_association: COLLABORATOR
diff_hunk: @@ -49,15 +52,11 @@ public ApplicationController(ProxyContext context) {
     }
 
     public Future<?> getApplication(String applicationId) {
-        boolean propertyFilteringRequired = !applicationId.equals(context.getDecodedSourceDeployment());
         vertx.executeBlocking(() -> deploymentService.findDeployment(context, applicationId), false)
                 .map(deployment -> {
                     if (deployment instanceof Application application) {
-                        if (propertyFilteringRequired) {
-                            application = ApplicationTypeSchemaUtils.filterCustomClientProperties(context.getConfig(), application);
-                        }
-                        application = ApplicationTypeSchemaUtils.modifyEndpointsForCustomApplication(context.getConfig(), application);
-                        return application;
+                        boolean applicationRequestInfoAboutItSelf = applicationId.equals(context.getDecodedSourceDeployment());
path: server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java
user: login: astsiapanay
body: Please add API tests to cover changes in the listing logic
created_at: 2025-04-26T06:43:26Z
author_association: COLLABORATOR
diff_hunk: @@ -164,7 +164,14 @@ private Application clearApplicationProperties(Application application, Config c
             features.setTokenizeEndpoint(null);
             features.setTruncatePromptEndpoint(null);
         }
-        return ApplicationTypeSchemaUtils.filterCustomClientProperties(config, application);
+        try {
path: server/src/main/java/com/epam/aidial/core/server/controller/ResourceController.java
user: login: astsiapanay
body: Please add API tests
created_at: 2025-04-26T06:43:51Z
author_association: COLLABORATOR
diff_hunk: @@ -33,6 +34,9 @@ public Boolean hasApplicationTypeSchemaId() {
         return applicationTypeSchemaId != null;
     }
 
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    private Boolean isInvalidApplication = null;
path: config/src/main/java/com/epam/aidial/core/config/Application.java
user: login: astsiapanay
body: Please update README to reflect changes in the app config
created_at: 2025-04-26T06:44:57Z
author_association: COLLABORATOR
diff_hunk: @@ -33,6 +34,9 @@ public Boolean hasApplicationTypeSchemaId() {
         return applicationTypeSchemaId != null;
     }
 
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    private Boolean isInvalidApplication = null;
path: config/src/main/java/com/epam/aidial/core/config/Application.java
user: login: sergey-zinchenko
body: done
created_at: 2025-04-26T07:51:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -33,6 +34,9 @@ public Boolean hasApplicationTypeSchemaId() {
         return applicationTypeSchemaId != null;
     }
 
+    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
+    private Boolean isInvalidApplication = null;
path: config/src/main/java/com/epam/aidial/core/config/Application.java
user: login: sergey-zinchenko
body: Not applicable. We won't read that value from the config. It is used to mark the application invalid if parsing failed.
created_at: 2025-04-26T07:52:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +67,20 @@ public Future<?> getApplication(String applicationId) {
         return Future.succeededFuture();
     }
 
+    private Application modifySchemaRichApplication(Application application, boolean propertyFilteringRequired) {
path: server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java
user: login: sergey-zinchenko
body: done
created_at: 2025-04-26T08:11:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -671,8 +684,8 @@ private String deploymentLockKey(ResourceDescriptor resource) {
 
     private String encodeTargetFolder(ResourceDescriptor resource, String id) {
         String location = resource.getBucketLocation()
-                + DEPLOYMENTS_NAME + ResourceDescriptor.PATH_SEPARATOR
-                + id + ResourceDescriptor.PATH_SEPARATOR;
+                          + DEPLOYMENTS_NAME + ResourceDescriptor.PATH_SEPARATOR
path: server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java
user: login: sergey-zinchenko
body: done
created_at: 2025-04-26T08:18:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -164,7 +164,14 @@ private Application clearApplicationProperties(Application application, Config c
             features.setTokenizeEndpoint(null);
             features.setTruncatePromptEndpoint(null);
         }
-        return ApplicationTypeSchemaUtils.filterCustomClientProperties(config, application);
+        try {
path: server/src/main/java/com/epam/aidial/core/server/controller/ResourceController.java
user: login: sergey-zinchenko
body: done
created_at: 2025-04-26T09:27:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -49,15 +52,11 @@ public ApplicationController(ProxyContext context) {
     }
 
     public Future<?> getApplication(String applicationId) {
-        boolean propertyFilteringRequired = !applicationId.equals(context.getDecodedSourceDeployment());
         vertx.executeBlocking(() -> deploymentService.findDeployment(context, applicationId), false)
                 .map(deployment -> {
                     if (deployment instanceof Application application) {
-                        if (propertyFilteringRequired) {
-                            application = ApplicationTypeSchemaUtils.filterCustomClientProperties(context.getConfig(), application);
-                        }
-                        application = ApplicationTypeSchemaUtils.modifyEndpointsForCustomApplication(context.getConfig(), application);
-                        return application;
+                        boolean applicationRequestInfoAboutItSelf = applicationId.equals(context.getDecodedSourceDeployment());
path: server/src/main/java/com/epam/aidial/core/server/controller/ApplicationController.java
user: login: sergey-zinchenko
body: done
created_at: 2025-04-26T10:13:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -229,6 +221,27 @@ public List<Application> getApplications(ResourceDescriptor resource,
         return applications;
     }
 
+    private Application extractApplicationFromResource(ResourceDescriptor resource, ResourceDescriptor item, ProxyContext ctx, MetadataBase meta) {
+        Application application = getApplication(item).getValue();
+        return modifySchemRichApplication(resource, ctx, application, item);
+    }
+
+    private static Application modifySchemRichApplication(ResourceDescriptor resource, ProxyContext ctx, Application application, ResourceDescriptor item) {
path: server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java
user: login: astsiapanay
body: Can we replace the method with `ApplicationTypeSchemaUtils.modifySchemaRichApplication`?
created_at: 2025-04-26T11:29:38Z
author_association: COLLABORATOR
diff_hunk: @@ -229,6 +221,27 @@ public List<Application> getApplications(ResourceDescriptor resource,
         return applications;
     }
 
+    private Application extractApplicationFromResource(ResourceDescriptor resource, ResourceDescriptor item, ProxyContext ctx, MetadataBase meta) {
+        Application application = getApplication(item).getValue();
+        return modifySchemRichApplication(resource, ctx, application, item);
+    }
+
+    private static Application modifySchemRichApplication(ResourceDescriptor resource, ProxyContext ctx, Application application, ResourceDescriptor item) {
path: server/src/main/java/com/epam/aidial/core/server/service/ApplicationService.java
user: login: sergey-zinchenko
body: Nop. It calls a different method. It is with a very similar name but different
created_at: 2025-04-26T11:35:00Z
author_association: CONTRIBUTOR

PR comments:
user: login: ai-dial-actions
created_at: 2025-04-25T22:00:02Z
body: <h1>Dependency Review</h1>
✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.<h2>OpenSSF Scorecard</h2>
<table><tr><th>Package</th><th>Version</th><th>Score</th><th>Details</th></tr>
</table><h2>Scanned Files</h2>
<ul>
<!-- Sticky Pull Request Commentdependency-analysis -->
author_association: CONTRIBUTOR
user: login: astsiapanay
created_at: 2025-04-26T11:42:53Z
body: /deploy-review
>Environment: [review-environment](https://chat-ai-dial-core-pr-801.nightly-test.deltixhub.io) | [pipeline](https://gitlab.deltixhub.com/Clients/deltix/nightly-test/dial/review/-/pipelines/602440)
>Overlay E2E tests status: [success](https://gitlab.deltixhub.com/Clients/deltix/nightly-test/dial/review/-/jobs/1809732)
>Chat E2E tests status: [success](https://gitlab.deltixhub.com/Clients/deltix/nightly-test/dial/review/-/jobs/1809731)
author_association: COLLABORATOR
user: login: sr-remsha
created_at: 2025-05-29T08:03:46Z
body: @sergey-zinchenko @astsiapanay do we need to update [API reference](https://dialx.ai/dial_api#tag/Applications/operation/saveCustomApplication) in any way?
author_association: CONTRIBUTOR
user: login: astsiapanay
created_at: 2025-07-23T08:55:43Z
body: @sr-remsha 
yes, we need to include the flag `invalid` in JSON response on getApplication and getCustom Application and listing APIs.
author_association: COLLABORATOR
user: login: sr-remsha
created_at: 2025-07-23T09:17:31Z
body: need to update getApplication and getCustom Application 
author_association: CONTRIBUTOR