diff --git a/openmetadata-service/pom.xml b/openmetadata-service/pom.xml
index 2e5202fef329..7aea1ae9a4d8 100644
--- a/openmetadata-service/pom.xml
+++ b/openmetadata-service/pom.xml
@@ -294,6 +294,10 @@
io.dropwizard.modules
dropwizard-web
+
+ com.github.erosb
+ everit-json-schema
+
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java b/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java
index becbd478d591..983357d9b5c4 100644
--- a/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/resources/types/TypeResource.java
@@ -26,6 +26,8 @@
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import java.util.UUID;
import javax.json.JsonPatch;
import javax.validation.Valid;
@@ -68,6 +70,7 @@
import org.openmetadata.service.util.JsonUtils;
import org.openmetadata.service.util.RestUtil.PutResponse;
import org.openmetadata.service.util.ResultList;
+import org.openmetadata.service.util.SchemaFieldExtractor;
@Path("/v1/metadata/types")
@Tag(
@@ -466,6 +469,57 @@ public Response addOrUpdateProperty(
return response.toResponse();
}
+ @GET
+ @Path("/fields/{entityType}")
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getEntityTypeFields(
+ @Context UriInfo uriInfo,
+ @Context SecurityContext securityContext,
+ @PathParam("entityType") String entityType,
+ @QueryParam("include") @DefaultValue("non-deleted") Include include) {
+
+ try {
+ Fields fieldsParam = new Fields(Set.of("customProperties"));
+ Type typeEntity = repository.getByName(uriInfo, entityType, fieldsParam, include, false);
+ SchemaFieldExtractor extractor = new SchemaFieldExtractor();
+ List fieldsList =
+ extractor.extractFields(typeEntity, entityType);
+ return Response.ok(fieldsList).type(MediaType.APPLICATION_JSON).build();
+
+ } catch (Exception e) {
+ LOG.error("Error processing schema for entity type: " + entityType, e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity(
+ "Error processing schema for entity type: "
+ + entityType
+ + ". Exception: "
+ + e.getMessage())
+ .build();
+ }
+ }
+
+ @GET
+ @Path("/customProperties")
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getAllCustomPropertiesByEntityType(
+ @Context UriInfo uriInfo, @Context SecurityContext securityContext) {
+ try {
+ SchemaFieldExtractor extractor = new SchemaFieldExtractor();
+ Map> customPropertiesMap =
+ extractor.extractAllCustomProperties(uriInfo, repository);
+ return Response.ok(customPropertiesMap).build();
+ } catch (Exception e) {
+ LOG.error("Error fetching custom properties: {}", e.getMessage(), e);
+ return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
+ .entity(
+ "Error processing schema for entity type: "
+ + entityType
+ + ". Exception: "
+ + e.getMessage())
+ .build();
+ }
+ }
+
private Type getType(CreateType create, String user) {
return repository
.copy(new Type(), create, user)
diff --git a/openmetadata-service/src/main/java/org/openmetadata/service/util/SchemaFieldExtractor.java b/openmetadata-service/src/main/java/org/openmetadata/service/util/SchemaFieldExtractor.java
new file mode 100644
index 000000000000..a53c640e1fac
--- /dev/null
+++ b/openmetadata-service/src/main/java/org/openmetadata/service/util/SchemaFieldExtractor.java
@@ -0,0 +1,607 @@
+package org.openmetadata.service.util;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.ws.rs.core.UriInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.everit.json.schema.*;
+import org.everit.json.schema.loader.SchemaClient;
+import org.everit.json.schema.loader.SchemaLoader;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+import org.openmetadata.schema.entity.Type;
+import org.openmetadata.schema.entity.type.CustomProperty;
+import org.openmetadata.schema.type.Include;
+import org.openmetadata.sdk.exception.SchemaProcessingException;
+import org.openmetadata.service.jdbi3.TypeRepository;
+
+@Slf4j
+public class SchemaFieldExtractor {
+
+ public SchemaFieldExtractor() {}
+
+ public List extractFields(Type typeEntity, String entityType)
+ throws SchemaProcessingException {
+ String schemaPath = determineSchemaPath(entityType);
+ String schemaUri = "classpath:///" + schemaPath;
+ SchemaClient schemaClient = new CustomSchemaClient(schemaUri);
+ Map fieldTypesMap = new LinkedHashMap<>();
+ Deque processingStack = new ArrayDeque<>();
+ Set processedFields = new HashSet<>();
+ Schema mainSchema = loadMainSchema(schemaPath, entityType, schemaUri, schemaClient);
+ extractFieldsFromSchema(mainSchema, "", fieldTypesMap, processingStack, processedFields);
+ addCustomProperties(
+ typeEntity, schemaUri, schemaClient, fieldTypesMap, processingStack, processedFields);
+ return convertMapToFieldList(fieldTypesMap);
+ }
+
+ public Map> extractAllCustomProperties(
+ UriInfo uriInfo, TypeRepository repository) {
+ Map> entityTypeToFields = new HashMap<>();
+ List entityTypes = getAllEntityTypes();
+
+ for (String entityType : entityTypes) {
+ String schemaPath = determineSchemaPath(entityType);
+ String schemaUri = "classpath:///" + schemaPath;
+ SchemaClient schemaClient = new CustomSchemaClient(schemaUri);
+ EntityUtil.Fields fieldsParam = new EntityUtil.Fields(Set.of("customProperties"));
+ Type typeEntity = repository.getByName(uriInfo, entityType, fieldsParam, Include.ALL, false);
+ Map fieldTypesMap = new LinkedHashMap<>();
+ Set processedFields = new HashSet<>();
+ Deque processingStack = new ArrayDeque<>();
+ addCustomProperties(
+ typeEntity, schemaUri, schemaClient, fieldTypesMap, processingStack, processedFields);
+ entityTypeToFields.put(entityType, convertMapToFieldList(fieldTypesMap));
+ }
+
+ return entityTypeToFields;
+ }
+
+ public static List getAllEntityTypes() {
+ List entityTypes = new ArrayList<>();
+ try {
+ String schemaDirectory = "json/schema/entity/";
+ Enumeration resources =
+ SchemaFieldExtractor.class.getClassLoader().getResources(schemaDirectory);
+ while (resources.hasMoreElements()) {
+ URL resourceUrl = resources.nextElement();
+ Path schemaDirPath = Paths.get(resourceUrl.toURI());
+
+ Files.walk(schemaDirPath)
+ .filter(Files::isRegularFile)
+ .filter(path -> path.toString().endsWith(".json"))
+ .forEach(
+ path -> {
+ try (InputStream is = Files.newInputStream(path)) {
+ JSONObject jsonSchema = new JSONObject(new JSONTokener(is));
+ // Check if the schema is an entity type
+ if (isEntityType(jsonSchema)) {
+ String fileName = path.getFileName().toString();
+ String entityType =
+ fileName.substring(0, fileName.length() - 5); // Remove ".json"
+ entityTypes.add(entityType);
+ LOG.debug("Found entity type: {}", entityType);
+ }
+ } catch (Exception e) {
+ LOG.error("Error reading schema file {}: {}", path, e.getMessage());
+ }
+ });
+ }
+ } catch (Exception e) {
+ LOG.error("Error scanning schema directory: {}", e.getMessage());
+ }
+ return entityTypes;
+ }
+
+ private static boolean isEntityType(JSONObject jsonSchema) {
+ return "@om-entity-type".equals(jsonSchema.optString("$comment"));
+ }
+
+ private Schema loadMainSchema(
+ String schemaPath, String entityType, String schemaUri, SchemaClient schemaClient)
+ throws SchemaProcessingException {
+ InputStream schemaInputStream = getClass().getClassLoader().getResourceAsStream(schemaPath);
+ if (schemaInputStream == null) {
+ LOG.error("Schema file not found at path: {}", schemaPath);
+ throw new SchemaProcessingException(
+ "Schema file not found for entity type: " + entityType,
+ SchemaProcessingException.ErrorType.RESOURCE_NOT_FOUND);
+ }
+
+ JSONObject rawSchema = new JSONObject(new JSONTokener(schemaInputStream));
+ SchemaLoader schemaLoader =
+ SchemaLoader.builder()
+ .schemaJson(rawSchema)
+ .resolutionScope(schemaUri)
+ .schemaClient(schemaClient)
+ .build();
+
+ try {
+ Schema schema = schemaLoader.load().build();
+ LOG.debug("Schema '{}' loaded successfully.", schemaPath);
+ return schema;
+ } catch (Exception e) {
+ LOG.error("Error loading schema '{}': {}", schemaPath, e.getMessage());
+ throw new SchemaProcessingException(
+ "Error loading schema '" + schemaPath + "': " + e.getMessage(),
+ SchemaProcessingException.ErrorType.OTHER);
+ }
+ }
+
+ private void extractFieldsFromSchema(
+ Schema schema,
+ String parentPath,
+ Map fieldTypesMap,
+ Deque processingStack,
+ Set processedFields) {
+ if (processingStack.contains(schema)) {
+ LOG.debug(
+ "Detected cyclic reference at path '{}'. Skipping further processing of this schema.",
+ parentPath);
+ return;
+ }
+
+ processingStack.push(schema);
+ try {
+ if (schema instanceof ObjectSchema objectSchema) {
+ for (Map.Entry propertyEntry :
+ objectSchema.getPropertySchemas().entrySet()) {
+ String fieldName = propertyEntry.getKey();
+ Schema fieldSchema = propertyEntry.getValue();
+ String fullFieldName = parentPath.isEmpty() ? fieldName : parentPath + "." + fieldName;
+
+ if (processedFields.contains(fullFieldName)) {
+ LOG.debug(
+ "Field '{}' has already been processed. Skipping to prevent duplication.",
+ fullFieldName);
+ continue;
+ }
+
+ LOG.debug("Processing field '{}'", fullFieldName);
+
+ if (fieldSchema instanceof ReferenceSchema referenceSchema) {
+ handleReferenceSchema(
+ referenceSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
+ } else if (fieldSchema instanceof ArraySchema arraySchema) {
+ handleArraySchema(
+ arraySchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
+ } else {
+ String fieldType = mapSchemaTypeToSimpleType(fieldSchema);
+ fieldTypesMap.putIfAbsent(fullFieldName, fieldType);
+ processedFields.add(fullFieldName);
+ LOG.debug("Added field '{}', Type: '{}'", fullFieldName, fieldType);
+ // Recursively process nested objects or arrays
+ if (fieldSchema instanceof ObjectSchema || fieldSchema instanceof ArraySchema) {
+ extractFieldsFromSchema(
+ fieldSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
+ }
+ }
+ }
+ } else if (schema instanceof ArraySchema arraySchema) {
+ handleArraySchema(arraySchema, parentPath, fieldTypesMap, processingStack, processedFields);
+ } else {
+ String fieldType = mapSchemaTypeToSimpleType(schema);
+ fieldTypesMap.putIfAbsent(parentPath, fieldType);
+ LOG.debug("Added field '{}', Type: '{}'", parentPath, fieldType);
+ }
+ } finally {
+ processingStack.pop();
+ }
+ }
+
+ private void handleReferenceSchema(
+ ReferenceSchema referenceSchema,
+ String fullFieldName,
+ Map fieldTypesMap,
+ Deque processingStack,
+ Set processedFields) {
+
+ String refUri = referenceSchema.getReferenceValue();
+ String referenceType = determineReferenceType(refUri);
+
+ if (referenceType != null) {
+ fieldTypesMap.putIfAbsent(fullFieldName, referenceType);
+ processedFields.add(fullFieldName);
+ LOG.debug("Added field '{}', Type: '{}'", fullFieldName, referenceType);
+ if (referenceType.startsWith("array<") && referenceType.endsWith(">")) {
+ Schema itemSchema =
+ referenceSchema.getReferredSchema() instanceof ArraySchema
+ ? ((ArraySchema) referenceSchema.getReferredSchema()).getAllItemSchema()
+ : referenceSchema.getReferredSchema();
+ extractFieldsFromSchema(
+ itemSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
+ } else if (!isPrimitiveType(referenceType)) {
+ Schema referredSchema = referenceSchema.getReferredSchema();
+ extractFieldsFromSchema(
+ referredSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
+ }
+ } else {
+ fieldTypesMap.putIfAbsent(fullFieldName, "object");
+ processedFields.add(fullFieldName);
+ LOG.debug("Added field '{}', Type: 'object'", fullFieldName);
+ extractFieldsFromSchema(
+ referenceSchema.getReferredSchema(),
+ fullFieldName,
+ fieldTypesMap,
+ processingStack,
+ processedFields);
+ }
+ }
+
+ private void handleArraySchema(
+ ArraySchema arraySchema,
+ String fullFieldName,
+ Map fieldTypesMap,
+ Deque processingStack,
+ Set processedFields) {
+
+ Schema itemsSchema = arraySchema.getAllItemSchema();
+
+ if (itemsSchema instanceof ReferenceSchema itemsReferenceSchema) {
+ String itemsRefUri = itemsReferenceSchema.getReferenceValue();
+ String itemsReferenceType = determineReferenceType(itemsRefUri);
+
+ if (itemsReferenceType != null) {
+ String arrayFieldType = "array<" + itemsReferenceType + ">";
+ fieldTypesMap.putIfAbsent(fullFieldName, arrayFieldType);
+ processedFields.add(fullFieldName);
+ LOG.debug("Added field '{}', Type: '{}'", fullFieldName, arrayFieldType);
+ Schema referredItemsSchema = itemsReferenceSchema.getReferredSchema();
+ extractFieldsFromSchema(
+ referredItemsSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
+ return;
+ }
+ }
+ String arrayType = mapSchemaTypeToSimpleType(itemsSchema);
+ fieldTypesMap.putIfAbsent(fullFieldName, "array<" + arrayType + ">");
+ processedFields.add(fullFieldName);
+ LOG.debug("Added field '{}', Type: 'array<{}>'", fullFieldName, arrayType);
+
+ if (itemsSchema instanceof ObjectSchema || itemsSchema instanceof ArraySchema) {
+ extractFieldsFromSchema(
+ itemsSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
+ }
+ }
+
+ private void addCustomProperties(
+ Type typeEntity,
+ String schemaUri,
+ SchemaClient schemaClient,
+ Map fieldTypesMap,
+ Deque processingStack,
+ Set processedFields) {
+ if (typeEntity == null || typeEntity.getCustomProperties() == null) {
+ return;
+ }
+
+ for (CustomProperty customProperty : typeEntity.getCustomProperties()) {
+ String propertyName = customProperty.getName();
+ String propertyType = customProperty.getPropertyType().getName();
+ String fullFieldName = propertyName; // No parent path for custom properties
+
+ LOG.debug("Processing custom property '{}'", fullFieldName);
+
+ if (isEntityReferenceList(propertyType)) {
+ String referenceType = "array";
+ fieldTypesMap.putIfAbsent(fullFieldName, referenceType);
+ processedFields.add(fullFieldName);
+ LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, referenceType);
+
+ Schema itemSchema = resolveSchemaByType("entityReference", schemaUri, schemaClient);
+ if (itemSchema != null) {
+ extractFieldsFromSchema(
+ itemSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
+ } else {
+ LOG.warn(
+ "Schema for type 'entityReference' not found. Skipping nested field extraction for '{}'.",
+ fullFieldName);
+ }
+ } else if (isEntityReference(propertyType)) {
+ String referenceType = "entityReference";
+ fieldTypesMap.putIfAbsent(fullFieldName, referenceType);
+ processedFields.add(fullFieldName);
+ LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, referenceType);
+
+ Schema referredSchema = resolveSchemaByType("entityReference", schemaUri, schemaClient);
+ if (referredSchema != null) {
+ extractFieldsFromSchema(
+ referredSchema, fullFieldName, fieldTypesMap, processingStack, processedFields);
+ } else {
+ LOG.warn(
+ "Schema for type 'entityReference' not found. Skipping nested field extraction for '{}'.",
+ fullFieldName);
+ }
+ } else {
+ fieldTypesMap.putIfAbsent(fullFieldName, propertyType);
+ processedFields.add(fullFieldName);
+ LOG.debug("Added custom property '{}', Type: '{}'", fullFieldName, propertyType);
+ }
+ }
+ }
+
+ private List convertMapToFieldList(Map fieldTypesMap) {
+ List fieldsList = new ArrayList<>();
+ for (Map.Entry entry : fieldTypesMap.entrySet()) {
+ fieldsList.add(new FieldDefinition(entry.getKey(), entry.getValue()));
+ }
+ return fieldsList;
+ }
+
+ private boolean isEntityReferenceList(String propertyType) {
+ return "entityReferenceList".equalsIgnoreCase(propertyType);
+ }
+
+ private boolean isEntityReference(String propertyType) {
+ return "entityReference".equalsIgnoreCase(propertyType);
+ }
+
+ private Schema resolveSchemaByType(String typeName, String schemaUri, SchemaClient schemaClient) {
+ String referencePath = determineReferencePath(typeName);
+ try {
+ return loadSchema(referencePath, schemaUri, schemaClient);
+ } catch (SchemaProcessingException e) {
+ LOG.error("Failed to load schema for type '{}': {}", typeName, e.getMessage());
+ return null;
+ }
+ }
+
+ private Schema loadSchema(String schemaPath, String schemaUri, SchemaClient schemaClient)
+ throws SchemaProcessingException {
+ InputStream schemaInputStream = getClass().getClassLoader().getResourceAsStream(schemaPath);
+ if (schemaInputStream == null) {
+ LOG.error("Schema file not found at path: {}", schemaPath);
+ throw new SchemaProcessingException(
+ "Schema file not found for path: " + schemaPath,
+ SchemaProcessingException.ErrorType.RESOURCE_NOT_FOUND);
+ }
+
+ JSONObject rawSchema = new JSONObject(new JSONTokener(schemaInputStream));
+ SchemaLoader schemaLoader =
+ SchemaLoader.builder()
+ .schemaJson(rawSchema)
+ .resolutionScope(schemaUri) // Base URI for resolving $ref
+ .schemaClient(schemaClient)
+ .build();
+
+ try {
+ Schema schema = schemaLoader.load().build();
+ LOG.debug("Schema '{}' loaded successfully.", schemaPath);
+ return schema;
+ } catch (Exception e) {
+ LOG.error("Error loading schema '{}': {}", schemaPath, e.getMessage());
+ throw new SchemaProcessingException(
+ "Error loading schema '" + schemaPath + "': " + e.getMessage(),
+ SchemaProcessingException.ErrorType.OTHER);
+ }
+ }
+
+ private String determineReferenceType(String refUri) {
+ // Pattern to extract the definition name if present
+ Pattern definitionPattern = Pattern.compile("^(?:.*/)?basic\\.json#/definitions/([\\w-]+)$");
+ Matcher matcher = definitionPattern.matcher(refUri);
+ if (matcher.find()) {
+ String definition = matcher.group(1);
+ return switch (definition) {
+ case "duration" -> "duration";
+ case "markdown" -> "markdown";
+ case "timestamp" -> "timestamp";
+ case "integer" -> "integer";
+ case "number" -> "number";
+ case "string" -> "string";
+ case "uuid" -> "uuid";
+ case "email" -> "email";
+ case "href" -> "href";
+ case "timeInterval" -> "timeInterval";
+ case "date" -> "date";
+ case "dateTime" -> "dateTime";
+ case "time" -> "time";
+ case "date-cp" -> "date-cp";
+ case "dateTime-cp" -> "dateTime-cp";
+ case "time-cp" -> "time-cp";
+ case "enum" -> "enum";
+ case "enumWithDescriptions" -> "enumWithDescriptions";
+ case "timezone" -> "timezone";
+ case "entityLink" -> "entityLink";
+ case "entityName" -> "entityName";
+ case "testCaseEntityName" -> "testCaseEntityName";
+ case "fullyQualifiedEntityName" -> "fullyQualifiedEntityName";
+ case "sqlQuery" -> "sqlQuery";
+ case "sqlFunction" -> "sqlFunction";
+ case "expression" -> "expression";
+ case "jsonSchema" -> "jsonSchema";
+ case "entityExtension" -> "entityExtension";
+ case "providerType" -> "providerType";
+ case "componentConfig" -> "componentConfig";
+ case "status" -> "status";
+ case "sourceUrl" -> "sourceUrl";
+ case "style" -> "style";
+ default -> {
+ LOG.warn("Unrecognized definition '{}' in refUri '{}'", definition, refUri);
+ yield "object";
+ }
+ };
+ }
+
+ // Existing file-based mappings
+ if (refUri.matches(".*basic\\.json$")) {
+ return "uuid";
+ }
+ if (refUri.matches(".*entityReference\\.json(?:#.*)?$")) {
+ return "entityReference";
+ }
+ if (refUri.matches(".*entityReferenceList\\.json(?:#.*)?$")) {
+ return "array";
+ }
+ if (refUri.matches(".*tagLabel\\.json(?:#.*)?$")) {
+ return "tagLabel";
+ }
+ if (refUri.matches(".*fullyQualifiedEntityName\\.json(?:#.*)?$")) {
+ return "fullyQualifiedEntityName";
+ }
+ if (refUri.matches(".*entityVersion\\.json(?:#.*)?$")) {
+ return "entityVersion";
+ }
+ if (refUri.matches(".*markdown\\.json(?:#.*)?$")) {
+ return "markdown";
+ }
+ if (refUri.matches(".*timestamp\\.json(?:#.*)?$")) {
+ return "timestamp";
+ }
+ if (refUri.matches(".*href\\.json(?:#.*)?$")) {
+ return "href";
+ }
+ if (refUri.matches(".*duration\\.json(?:#.*)?$")) {
+ return "duration";
+ }
+ return null;
+ }
+
+ private String mapSchemaTypeToSimpleType(Schema schema) {
+ if (schema == null) {
+ LOG.debug("Mapping type: null -> 'object'");
+ return "object";
+ }
+ if (schema instanceof StringSchema) {
+ LOG.debug("Mapping schema instance '{}' to 'string'", schema.getClass().getSimpleName());
+ return "string";
+ } else if (schema instanceof NumberSchema numberSchema) {
+ if (numberSchema.requiresInteger()) {
+ LOG.debug("Mapping schema instance '{}' to 'integer'", schema.getClass().getSimpleName());
+ return "integer";
+ } else {
+ LOG.debug("Mapping schema instance '{}' to 'number'", schema.getClass().getSimpleName());
+ return "number";
+ }
+ } else if (schema instanceof BooleanSchema) {
+ LOG.debug("Mapping schema instance '{}' to 'boolean'", schema.getClass().getSimpleName());
+ return "boolean";
+ } else if (schema instanceof ObjectSchema) {
+ LOG.debug("Mapping schema instance '{}' to 'object'", schema.getClass().getSimpleName());
+ return "object";
+ } else if (schema instanceof ArraySchema) {
+ LOG.debug("Mapping schema instance '{}' to 'array'", schema.getClass().getSimpleName());
+ return "array";
+ } else if (schema instanceof NullSchema) {
+ LOG.debug("Mapping schema instance '{}' to 'null'", schema.getClass().getSimpleName());
+ return "null";
+ } else {
+ LOG.debug(
+ "Mapping unknown schema instance '{}' to 'string'", schema.getClass().getSimpleName());
+ return "string";
+ }
+ }
+
+ private boolean isPrimitiveType(String type) {
+ return type.equals("string")
+ || type.equals("integer")
+ || type.equals("number")
+ || type.equals("boolean")
+ || type.equals("uuid")
+ || // Treat 'uuid' as a primitive type
+ type.equals("timestamp")
+ || type.equals("href")
+ || type.equals("duration")
+ || type.equals("date")
+ || type.equals("dateTime")
+ || type.equals("time")
+ || type.equals("date-cp")
+ || type.equals("dateTime-cp")
+ || type.equals("time-cp")
+ || type.equals("enum")
+ || type.equals("enumWithDescriptions")
+ || type.equals("timezone")
+ || type.equals("entityLink")
+ || type.equals("entityName")
+ || type.equals("testCaseEntityName")
+ || type.equals("fullyQualifiedEntityName")
+ || type.equals("sqlQuery")
+ || type.equals("sqlFunction")
+ || type.equals("expression")
+ || type.equals("jsonSchema")
+ || type.equals("entityExtension")
+ || type.equals("providerType")
+ || type.equals("componentConfig")
+ || type.equals("status")
+ || type.equals("sourceUrl")
+ || type.equals("style");
+ }
+
+ private String determineReferencePath(String typeName) {
+ String baseSchemaDirectory = "json/schema/entity/";
+ String schemaFileName = typeName + ".json";
+ return baseSchemaDirectory + schemaFileName;
+ }
+
+ private String determineSchemaPath(String entityType) {
+ String subdirectory = getEntitySubdirectory(entityType);
+ return "json/schema/entity/" + subdirectory + "/" + entityType + ".json";
+ }
+
+ private String getEntitySubdirectory(String entityType) {
+ Map entityTypeToSubdirectory =
+ Map.of(
+ "dashboard", "data",
+ "table", "data",
+ "pipeline", "services",
+ "votes", "data");
+ return entityTypeToSubdirectory.getOrDefault(entityType, "data");
+ }
+
+ @Slf4j
+ private static class CustomSchemaClient implements SchemaClient {
+ private final String baseUri;
+
+ public CustomSchemaClient(String baseUri) {
+ this.baseUri = baseUri;
+ }
+
+ @Override
+ public InputStream get(String url) {
+ LOG.debug("SchemaClient: Resolving URL '{}' against base URI '{}'", url, baseUri);
+ String resourcePath = mapUrlToResourcePath(url);
+ LOG.debug("SchemaClient: Loading resource from path '{}'", resourcePath);
+ InputStream is = getClass().getClassLoader().getResourceAsStream(resourcePath);
+ if (is == null) {
+ LOG.error("Resource not found: {}", resourcePath);
+ throw new RuntimeException("Resource not found: " + resourcePath);
+ }
+ return is;
+ }
+
+ private String mapUrlToResourcePath(String url) {
+ if (url.startsWith("https://open-metadata.org/schema/")) {
+ String relativePath = url.substring("https://open-metadata.org/schema/".length());
+ return "json/schema/" + relativePath;
+ } else {
+ throw new RuntimeException("Unsupported URL: " + url);
+ }
+ }
+ }
+
+ @lombok.Getter
+ @lombok.Setter
+ public static class FieldDefinition {
+ private String name;
+ private String type;
+
+ public FieldDefinition(String name, String type) {
+ this.name = name;
+ this.type = type;
+ }
+ }
+}
diff --git a/openmetadata-spec/src/main/java/org/openmetadata/sdk/exception/SchemaProcessingException.java b/openmetadata-spec/src/main/java/org/openmetadata/sdk/exception/SchemaProcessingException.java
new file mode 100644
index 000000000000..5ecf05101e40
--- /dev/null
+++ b/openmetadata-spec/src/main/java/org/openmetadata/sdk/exception/SchemaProcessingException.java
@@ -0,0 +1,24 @@
+package org.openmetadata.sdk.exception;
+
+import lombok.Getter;
+
+@Getter
+public class SchemaProcessingException extends Exception {
+ public enum ErrorType {
+ RESOURCE_NOT_FOUND,
+ UNSUPPORTED_URL,
+ OTHER
+ }
+
+ private final ErrorType errorType;
+
+ public SchemaProcessingException(String message, ErrorType errorType) {
+ super(message);
+ this.errorType = errorType;
+ }
+
+ public SchemaProcessingException(String message, Throwable cause, ErrorType errorType) {
+ super(message, cause);
+ this.errorType = errorType;
+ }
+}
diff --git a/pom.xml b/pom.xml
index 9be45980f522..54d7c4207c71 100644
--- a/pom.xml
+++ b/pom.xml
@@ -156,6 +156,7 @@
1.2.13
1.2.13
2.9.0
+ 1.14.4
@@ -517,7 +518,11 @@
picocli
${picocli.version}
-
+
+ com.github.erosb
+ everit-json-schema
+ ${everit.version}
+
org.eclipse.jetty