Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b99f83a
fix: validate custom property name charset
sonika-shah Apr 29, 2026
d4c2013
Update generated TypeScript types
github-actions[bot] Apr 29, 2026
9b7eb18
test: add schema-vs-Java consistency test for custom property name
sonika-shah Apr 29, 2026
ec6582f
fix: extend custom property name charset after gap-coverage matrix
sonika-shah Apr 29, 2026
452bc25
Update generated TypeScript types
github-actions[bot] Apr 29, 2026
9aba5b3
updated the custom property name validation
Rohit0301 Apr 29, 2026
021647a
added name suffix in custom property name
Rohit0301 Apr 29, 2026
a68bd03
lint fixes
Rohit0301 Apr 29, 2026
4f033ea
include backslash in invalid char
Rohit0301 Apr 30, 2026
be23e01
fixed the playwright issue
Rohit0301 Apr 30, 2026
1823307
lint fix
Rohit0301 Apr 30, 2026
28ec255
Merge branch 'main' into fix/custom-property-name-charset-validation
Rohit0301 May 2, 2026
d490b3d
Merge branch 'main' into fix/custom-property-name-charset-validation
sonika-shah May 4, 2026
fa82046
fix check style
sonika-shah May 4, 2026
3c92e1b
Drop redundant Java validator for custom property name; tighten IT as…
sonika-shah May 4, 2026
ef4d807
restrict few more special characters from Cp name
Rohit0301 May 5, 2026
0129bf1
minor fix
Rohit0301 May 5, 2026
34f3163
Disallow & < > in custom property names; align IT cases
sonika-shah May 5, 2026
5c3fbe2
Update generated TypeScript types
github-actions[bot] May 5, 2026
0dd1684
Close PATCH bypass for custom property name validation on Type
sonika-shah May 5, 2026
722f2f2
Block * in custom property names — breaks ES field-path lookup
sonika-shah May 5, 2026
1864b0a
Validate only newly-added custom properties; isolate PATCH IT to fres…
sonika-shah May 5, 2026
40738ed
chore: prettier formatting on the new asterisk-rejection test
sonika-shah May 5, 2026
04eef82
Potential fix for pull request finding
Rohit0301 May 5, 2026
dd494e9
docs: add + ? ~ ` to JSDoc allow-list to match the regex
sonika-shah May 5, 2026
d46af48
fix(it): request customProperties field on read-back in PATCH IT
sonika-shah May 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
import org.openmetadata.schema.type.CustomPropertyConfig;
import org.openmetadata.schema.type.customProperties.EnumConfig;
import org.openmetadata.sdk.client.OpenMetadataClient;
import org.openmetadata.sdk.exceptions.InvalidRequestException;
import org.openmetadata.sdk.network.HttpMethod;
import org.openmetadata.sdk.network.RequestOptions;

/**
* Integration tests for Type entity operations.
Expand Down Expand Up @@ -324,6 +326,224 @@ void test_typeWithInvalidName_fails(TestNamespace ns) {
}
}

@Test
void test_customPropertyNameAllowedCharacters_succeeds(TestNamespace ns) throws Exception {
OpenMetadataClient client = SdkClients.adminClient();
UUID tableTypeId = TABLE_ENTITY_TYPE.getId();
String prefix = ns.prefix("safe");

String[] allowedNames = {
prefix + "Plain",
prefix + "_underscore",
prefix + "-hyphen",
prefix + ".dot",
prefix + "with space",
prefix + "with/slash",
prefix + "with%percent",
prefix + "with#hash",
prefix + "with@at",
prefix + "with!bang",
prefix + "with,comma",
prefix + "with;semi",
prefix + "with=eq",
prefix + "with|pipe",
prefix + "with'quote",
prefix + "with(lparen",
prefix + "with)rparen",
prefix + "with[lbrack",
prefix + "with]rbrack",
prefix + "with{lbrace",
prefix + "with}rbrace",
prefix + "with+plus",
prefix + "with?question",
prefix + "with~tilde",
prefix + "with`backtick",
prefix + "withMatched(pair)",
prefix + "withDigits123",
};

for (String name : allowedNames) {
CustomProperty property = new CustomProperty();
property.setName(name);
property.setDescription("Allowed-charset test for custom property name");
property.setPropertyType(STRING_TYPE.getEntityReference());

Type updatedType = addCustomProperty(client, tableTypeId, property);
assertNotNull(updatedType, "Allowed name '" + name + "' must be accepted");

boolean present =
updatedType.getCustomProperties().stream().anyMatch(cp -> name.equals(cp.getName()));
assertTrue(present, "Custom property '" + name + "' should be saved on the type");
}
}

@Test
void test_customPropertyNameDisallowedCharacters_fails(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();
UUID tableTypeId = TABLE_ENTITY_TYPE.getId();
String prefix = ns.prefix("bad");

String[] disallowedNames = {
prefix + "with\"dquote",
prefix + "with:colon",
prefix + "with^caret",
prefix + "with$dollar",
prefix + "with\\backslash",
prefix + "with&amp",
prefix + "with<lt",
prefix + "with>gt",
prefix + "with*asterisk",
Comment thread
sonika-shah marked this conversation as resolved.
};

for (String name : disallowedNames) {
CustomProperty property = new CustomProperty();
property.setName(name);
property.setDescription("Disallowed-charset test for custom property name");
property.setPropertyType(STRING_TYPE.getEntityReference());

assertThrows(
InvalidRequestException.class,
() -> addCustomProperty(client, tableTypeId, property),
"Custom property name '" + name + "' should be rejected with HTTP 400");
}
}

@Test
void test_customPropertyNameMustStartWithAlphanumeric_fails(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();
UUID tableTypeId = TABLE_ENTITY_TYPE.getId();
String prefix = ns.prefix("lead");

String[] invalidLeads = {
"_" + prefix, "-" + prefix, "." + prefix, " " + prefix, "(" + prefix,
};

for (String name : invalidLeads) {
CustomProperty property = new CustomProperty();
property.setName(name);
property.setDescription("Leading-character validation");
property.setPropertyType(STRING_TYPE.getEntityReference());

assertThrows(
InvalidRequestException.class,
() -> addCustomProperty(client, tableTypeId, property),
"Custom property name '" + name + "' must start with alphanumeric (HTTP 400 expected)");
}
}

@Test
void test_customPropertyNameTooLong_fails(TestNamespace ns) {
OpenMetadataClient client = SdkClients.adminClient();
UUID tableTypeId = TABLE_ENTITY_TYPE.getId();

StringBuilder longName = new StringBuilder(ns.prefix("long"));
while (longName.length() <= 256) {
longName.append('a');
}

CustomProperty property = new CustomProperty();
property.setName(longName.toString());
property.setDescription("Length validation");
property.setPropertyType(STRING_TYPE.getEntityReference());

assertThrows(
InvalidRequestException.class,
() -> addCustomProperty(client, tableTypeId, property),
"Custom property name longer than 256 characters should be rejected with HTTP 400");
}

@Test
void test_customPropertyNameUnbalancedBrackets_succeeds(TestNamespace ns) throws Exception {
OpenMetadataClient client = SdkClients.adminClient();
UUID tableTypeId = TABLE_ENTITY_TYPE.getId();
String prefix = ns.prefix("bracket");

String[] unbalancedNames = {
prefix + "openParen(",
prefix + "closeParen)",
prefix + "openLbrack[",
prefix + "closeRbrack]",
prefix + "openLbrace{",
prefix + "closeRbrace}",
};

for (String name : unbalancedNames) {
CustomProperty property = new CustomProperty();
property.setName(name);
property.setDescription("Unbalanced-bracket validation");
property.setPropertyType(STRING_TYPE.getEntityReference());

Type updatedType = addCustomProperty(client, tableTypeId, property);
assertNotNull(updatedType);

boolean present =
updatedType.getCustomProperties().stream().anyMatch(cp -> name.equals(cp.getName()));
assertTrue(present, "Unbalanced bracket name '" + name + "' should be saved");
}
}

@Test
void test_patchCannotAddCustomPropertyWithDisallowedName(TestNamespace ns) throws Exception {
OpenMetadataClient client = SdkClients.adminClient();
Type fresh = createEntityTypeForTest(client, ns, "patchBadType");

String badName = ns.prefix("patched:bad");
String patchJson =
String.format(
"[{\"op\":\"add\",\"path\":\"/customProperties\","
+ "\"value\":[{\"name\":\"%s\",\"description\":\"probe\","
+ "\"propertyType\":{\"id\":\"%s\",\"type\":\"type\",\"name\":\"string\"}}]}]",
badName, STRING_TYPE.getId());

assertThrows(
InvalidRequestException.class,
() ->
client
.getHttpClient()
.executeForString(
HttpMethod.PATCH,
"/v1/metadata/types/" + fresh.getId(),
patchJson,
RequestOptions.builder()
.header("Content-Type", "application/json-patch+json")
.build()),
"PATCH that adds a custom property with disallowed character must return 400");

Type after = getTypeById(client, fresh.getId(), "customProperties");
boolean persisted =
after.getCustomProperties() != null
&& after.getCustomProperties().stream().anyMatch(cp -> badName.equals(cp.getName()));
assertFalse(persisted, "Bad-name custom property must not be persisted via PATCH");
}

@Test
void test_patchCanAddCustomPropertyWithValidName(TestNamespace ns) throws Exception {
OpenMetadataClient client = SdkClients.adminClient();
Type fresh = createEntityTypeForTest(client, ns, "patchGoodType");

String goodName = ns.prefix("patchedGood");
String patchJson =
String.format(
"[{\"op\":\"add\",\"path\":\"/customProperties\","
+ "\"value\":[{\"name\":\"%s\",\"description\":\"probe\","
+ "\"propertyType\":{\"id\":\"%s\",\"type\":\"type\",\"name\":\"string\"}}]}]",
goodName, STRING_TYPE.getId());

client
.getHttpClient()
.executeForString(
HttpMethod.PATCH,
"/v1/metadata/types/" + fresh.getId(),
patchJson,
RequestOptions.builder().header("Content-Type", "application/json-patch+json").build());

Type after = getTypeById(client, fresh.getId(), "customProperties");
boolean persisted =
after.getCustomProperties() != null
&& after.getCustomProperties().stream().anyMatch(cp -> goodName.equals(cp.getName()));
assertTrue(persisted, "Valid-name custom property added via PATCH should be persisted");
}

@Test
void test_getEntityTypeFields() throws Exception {
OpenMetadataClient client = SdkClients.adminClient();
Expand Down Expand Up @@ -770,6 +990,21 @@ private static Type createType(OpenMetadataClient client, CreateType createReque
.execute(HttpMethod.POST, "/v1/metadata/types", createRequest, Type.class);
}

/**
* Create a unique entity-category Type per test so PATCH-driven tests can mutate
* customProperties without racing against other tests on shared built-in types.
*/
private static Type createEntityTypeForTest(
OpenMetadataClient client, TestNamespace ns, String label) throws Exception {
CreateType req = new CreateType();
req.setName(ns.prefix(label));
req.setCategory(Category.Entity);
req.setDescription("Per-test entity type for PATCH IT");
req.setNameSpace("data");
req.setSchema("{}");
return createType(client, req);
}

private static Type getTypeById(OpenMetadataClient client, UUID typeId) throws Exception {
String response =
client
Expand All @@ -778,6 +1013,18 @@ private static Type getTypeById(OpenMetadataClient client, UUID typeId) throws E
return OBJECT_MAPPER.readValue(response, Type.class);
}

private static Type getTypeById(OpenMetadataClient client, UUID typeId, String fields)
throws Exception {
String response =
client
.getHttpClient()
.executeForString(
HttpMethod.GET,
"/v1/metadata/types/" + typeId.toString() + "?fields=" + fields,
null);
return OBJECT_MAPPER.readValue(response, Type.class);
}

private static Type getTypeByName(OpenMetadataClient client, String name) throws Exception {
String response =
client
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import org.openmetadata.service.util.EntityUtil.Fields;
import org.openmetadata.service.util.EntityUtil.RelationIncludes;
import org.openmetadata.service.util.RestUtil.PutResponse;
import org.openmetadata.service.util.ValidatorUtil;

@Slf4j
public class TypeRepository extends EntityRepository<Type> {
Expand Down Expand Up @@ -344,6 +345,13 @@ private void updateCustomProperties() {
List<CustomProperty> deleted = new ArrayList<>();
recordListChange(
"customProperties", origProperties, updatedProperties, added, deleted, customFieldMatch);
// Legacy names from existing data are not re-validated; only newly added ones.
for (CustomProperty property : added) {
String violations = ValidatorUtil.validate(property);
if (violations != null) {
throw new IllegalArgumentException(violations);
Comment thread
sonika-shah marked this conversation as resolved.
Comment thread
sonika-shah marked this conversation as resolved.
Comment thread
sonika-shah marked this conversation as resolved.
}
}
Comment thread
sonika-shah marked this conversation as resolved.
for (CustomProperty property : added) {
storeCustomProperty(property);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@
"maxLength": 256,
"pattern": "^((?!::).)*$"
},
"customPropertyName": {
"description": "Name of a custom property. Allowed characters: alphanumeric, _ - . / % # @ ! , ; = | ' + ? ~ ` space ( ) [ ] { }. Must start with an alphanumeric character. Disallowed characters: \" * & < > : ^ $ \\",
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^[A-Za-z0-9][A-Za-z0-9 _\\-.,;/%#@!'(){}\\[\\]|=+?~`]*$"
Comment thread
sonika-shah marked this conversation as resolved.
},
Comment thread
sonika-shah marked this conversation as resolved.
"testCaseEntityName": {
"description": "Name that identifies a test definition and test case.",
"type": "string",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@
},
"properties": {
"name": {
"description": "Name of the entity property. Note a property name must be unique for an entity. Property name must follow camelCase naming adopted by openMetadata - must start with lower case with no space, underscore, or dots.",
"$ref": "../type/basic.json#/definitions/entityName"
"description": "Name of the entity property. Must be unique for an entity. Allowed characters: alphanumeric, _ - . / % # @ ! , ; = | ' + ? ~ ` space ( ) [ ] { }. Must start with an alphanumeric character. Disallowed: \" * & < > : ^ $ \\.",
"$ref": "../type/basic.json#/definitions/customPropertyName"
},
"displayName": {
"description": "Display Name for the custom property.Must be unique for an entity.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const NAME_MIN_MAX_LENGTH_VALIDATION_ERROR =
export const NAME_MAX_LENGTH_VALIDATION_ERROR =
'Name size must be between 1 and 128';

export const CP_NAME_MAX_LENGTH_VALIDATION_ERROR =
'Name size must be between 1 and 256';

export const DELETE_TERM = 'DELETE';

export const COMMON_TIER_TAG = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,5 +212,19 @@ export const CUSTOM_PROPERTIES_ENTITIES = {
},
};

export const CUSTOM_PROPERTY_NAME_VALIDATION_ERROR =
"Name must not contain '::'.";
export const CUSTOM_PROPERTY_NAME_VALIDATION_ERROR = String.raw`Name must start with a letter or number. Invalid characters: " * : ^ $ \ < > &`;

export const CUSTOM_PROPERTY_INVALID_NAMES = {
STARTS_WITH_SPECIAL_CHAR: '_invalidName',
DISALLOWED_COLON: 'name:with:colon',
DISALLOWED_DOLLAR: 'name$invalid',
DISALLOWED_CARET: 'name^invalid',
DISALLOWED_QUOTE: 'name"invalid',
DISALLOWED_BACKSLASH: String.raw`name\invalid`,
DISALLOWED_LESS_THAN: 'name<<invalid',
DISALLOWED_GREATER_THAN: 'name>>invalid',
DISALLOWED_AMPERSAND: 'name&invalid',
DISALLOWED_ASTERISK: 'name*invalid',
};

export const NAME_SUFFIX = ".!@#%`()_-=+{}[]~|;',.?/";
Loading
Loading