ObjectType Deep Dive: Property Type System and Constraints
In traditional databases, you only have limited types like VARCHAR, INT, DECIMAL, TIMESTAMP. When business needs to express "this field can only be one of 3 values" or "this field is a nested address structure," you must handle it in the application layer.
ObjectType Deep Dive: Property Type System and Constraints
“Series: S4 Ontology Modeling · Article 2 | Level: Intermediate | Reading Time: 18 min
#TL;DR
- coomia-dip provides 20+ property types, from basic scalars (STRING / INTEGER / DOUBLE) to composite types (STRUCT / ARRAY / MAP / ENUM), covering all industrial-grade modeling needs.
- The constraint system supports three levels: field-level (required / unique / range / pattern), object-level (cross-field rules), and relation-level (referential integrity) — ensuring data quality at the Schema layer, not the application layer.
- Primary key design has 4 strategies (natural key / UUID / composite key / sequence key). Poor choices lead to data distribution skew and query performance degradation; this article provides a decision matrix.
#1. Why the Property Type System Matters
In traditional databases, you only have limited types like VARCHAR, INT, DECIMAL, TIMESTAMP. When business needs to express "this field can only be one of 3 values" or "this field is a nested address structure," you must handle it in the application layer.
coomia-dip's ObjectType shifts type checking to the Schema layer:
Traditional Approach coomia-dip Approach
┌─────────────┐ ┌──────────────────────┐
│ Database │ │ SchemaRegistry │
│ VARCHAR(50) │ → Runtime check │ STRING(maxLen=50) │ → Definition-time
│ INT │ App-layer valid. │ ENUM(A,B,C) │ Schema validation
│ TEXT(JSON) │ No struct guaranty │ STRUCT(Address) │ Struct guaranteed
└─────────────┘ └──────────────────────┘
#2. Complete Property Type Catalog
#2.1 Scalar Types
| Type | Description | Default Constraints | Storage Mapping |
|---|---|---|---|
STRING | Text string | maxLength: 65535 | VARCHAR |
INTEGER | 32-bit integer | min: -2^31, max: 2^31-1 | INT |
LONG | 64-bit integer | min: -2^63, max: 2^63-1 | BIGINT |
DOUBLE | 64-bit floating point | None | DOUBLE |
DECIMAL | Exact decimal | precision: 38, scale: 10 | DECIMAL |
BOOLEAN | Boolean | None | BOOLEAN |
DATE | Date | format: ISO-8601 | DATE |
TIMESTAMP | Timestamp | timezone: UTC | TIMESTAMP |
TIME | Time | format: HH:mm:ss | TIME |
DURATION | Time duration | format: ISO-8601 | VARCHAR |
# Scalar type examples
properties:
name:
type: STRING
required: true
constraints:
minLength: 1
maxLength: 200
price:
type: DECIMAL
constraints:
precision: 10
scale: 2
min: 0.00
createdAt:
type: TIMESTAMP
defaultValue: "$now" # Special variable: current time
isActive:
type: BOOLEAN
defaultValue: true
#2.2 Enum Type
properties:
status:
type: ENUM
enumValues:
- value: DRAFT
displayName: "Draft"
description: "Initial state"
- value: ACTIVE
displayName: "Active"
description: "Currently in use"
- value: DEPRECATED
displayName: "Deprecated"
description: "Planned for retirement"
- value: ARCHIVED
displayName: "Archived"
description: "No longer available"
defaultValue: DRAFT
transitions: # Optional: state machine constraints
DRAFT: [ACTIVE]
ACTIVE: [DEPRECATED]
DEPRECATED: [ARCHIVED]
ENUM type vs STRING + app-layer validation:
STRING + App Layer: ENUM Type:
┌───────────────────┐ ┌───────────────────┐
│ DB stores "actve" │ ← Typo │ DB stores "ACTIVE" │ ← Schema validated
│ DB stores "Active" │ ← Case │ Frontend gets list │ ← Auto UI
│ Frontend hardcodes │ │ API auto-validates │ ← No hand-writing
│ Each service dups │ │ State machine opt. │ ← Declarative
└───────────────────┘ └───────────────────┘
#2.3 Composite Types
properties:
# STRUCT: Nested structure
address:
type: STRUCT
structType: Address # References a registered StructType
# ARRAY: List
tags:
type: ARRAY
itemType: STRING
constraints:
maxItems: 20
uniqueItems: true # No duplicate elements
# MAP: Key-value mapping
metadata:
type: MAP
keyType: STRING
valueType: STRING
constraints:
maxEntries: 50
# REFERENCE: Reference to another ObjectType
ownerId:
type: REFERENCE
targetType: User
onDelete: SET_NULL # Cascade strategy
# ATTACHMENT: File attachment
documents:
type: ARRAY
itemType: ATTACHMENT
constraints:
maxFileSize: "10MB"
allowedMimeTypes:
- "application/pdf"
- "image/*"
#2.4 Special Types
properties:
# GEO_POINT: Geographic coordinate
location:
type: GEO_POINT
constraints:
srid: 4326 # WGS84 coordinate system
# GEO_SHAPE: Geographic shape
boundary:
type: GEO_SHAPE
constraints:
allowedShapes: [POLYGON, MULTI_POLYGON]
# VECTOR: Vector embedding
embedding:
type: VECTOR
constraints:
dimensions: 768 # Vector dimensions
similarity: COSINE # Similarity calculation method
# TIMESERIES: Time series data reference
temperatureHistory:
type: TIMESERIES
valueType: DOUBLE
resolution: "1m" # 1-minute resolution
# EXPRESSION: Computed expression
displayLabel:
type: EXPRESSION
expression: "concat(name, ' (', code, ')')"
#2.5 Complete Type Hierarchy
Property Types
│
┌───────────────┼───────────────┐
│ │ │
Scalar Composite Special
│ │ │
┌─────────┼─────────┐ ┌─┼─────┐ ┌───┼─────┐
│ │ │ │ │ │ │ │ │ │ │
STRING INT LONG DOUBLE BOOL STRUCT ARRAY GEO VECTOR TIMESERIES
DATE TIMESTAMP DECIMAL MAP REFERENCE EXPRESSION
TIME DURATION ENUM ATTACHMENT
#3. Constraint System Deep Dive
#3.1 Field-Level Constraints
properties:
email:
type: STRING
required: true # Not-null constraint
unique: true # Uniqueness constraint
immutable: false # Mutable after creation
constraints:
pattern: "^[\\w.-]+@[\\w.-]+\\.\\w+$" # Regex constraint
maxLength: 255
age:
type: INTEGER
constraints:
min: 0 # Minimum value
max: 200 # Maximum value
score:
type: DOUBLE
constraints:
min: 0.0
max: 100.0
multipleOf: 0.5 # Must be multiple of 0.5
startDate:
type: DATE
constraints:
futureOnly: true # Only future dates allowed
Constraint Quick Reference:
| Constraint | Applicable Types | Description |
|---|---|---|
required | All | Not null |
unique | All scalars | Globally unique |
immutable | All | Cannot change after creation |
defaultValue | All | Default value |
min / max | Numeric, date | Range limit |
minLength / maxLength | STRING | Length limit |
pattern | STRING | Regex match |
multipleOf | Numeric | Multiple constraint |
futureOnly / pastOnly | DATE, TIMESTAMP | Time direction |
maxItems / minItems | ARRAY | Array length |
uniqueItems | ARRAY | Element uniqueness |
maxEntries | MAP | Map entry count |
allowedMimeTypes | ATTACHMENT | File type whitelist |
maxFileSize | ATTACHMENT | File size limit |
#3.2 Object-Level Constraints
Cross-field business rules:
spec:
properties:
startDate:
type: DATE
endDate:
type: DATE
minQuantity:
type: INTEGER
maxQuantity:
type: INTEGER
constraints:
# Cross-field constraint
- name: dateRange
type: COMPARISON
expression: "startDate <= endDate"
message: "End date must be after start date"
- name: quantityRange
type: COMPARISON
expression: "minQuantity <= maxQuantity"
message: "Max quantity must be >= min quantity"
# Conditional required
- name: conditionalRequired
type: CONDITIONAL
when: "status == 'ACTIVE'"
then:
required: ["activatedBy", "activatedAt"]
message: "Activated status requires activator and activation time"
# Mutual exclusion
- name: mutualExclusive
type: MUTUAL_EXCLUSIVE
fields: ["internalCode", "externalCode"]
message: "Internal and external code cannot both be empty"
# Custom rule
- name: luhnCheck
type: CUSTOM
expression: "luhn_check(cardNumber)"
message: "Card number checksum is incorrect"
#3.3 Relation-Level Constraints
# Referential integrity
relations:
- name: OrderBelongsToCustomer
from: Order
to: Customer
cardinality: MANY_TO_ONE
constraints:
required: true # Every Order must have a Customer
onDelete: RESTRICT # Cannot delete Customer with Orders
onUpdate: CASCADE # Cascade on Customer PK change
# Cardinality constraints
- name: TeamHasMembers
from: Team
to: Employee
cardinality: ONE_TO_MANY
constraints:
minCount: 1 # Team must have at least 1 member
maxCount: 50 # Team can have at most 50 members
#4. Primary Key Design Strategies
#4.1 Four Strategies Compared
┌──────────────┬─────────────┬────────────┬────────────┐
│ Strategy │ Example │ Pros │ Cons │
├──────────────┼─────────────┼────────────┼────────────┤
│ Natural Key │ email │ Readable │ May change │
│ │ isbn │ No gen needed│ Data skew │
├──────────────┼─────────────┼────────────┼────────────┤
│ UUID │ uuid-v4 │ Globally │ Index bloat │
│ │ │ unique │ Not readable│
├──────────────┼─────────────┼────────────┼────────────┤
│ Composite │ (tenant,id) │ Natural │ Complex │
│ │ │ partitioning│ queries │
├──────────────┼─────────────┼────────────┼────────────┤
│ Sequence │ BIGINT auto │ Compact │ Not distrib │
│ │ │ Ordered │ Info leak │
└──────────────┴─────────────┴────────────┴────────────┘
#4.2 coomia-dip Recommendation: Semantic ID
spec:
primaryKey:
property: equipmentId
strategy: SEMANTIC_ID
config:
prefix: "EQ" # Type prefix
separator: "-"
segments:
- type: PROPERTY
source: factoryCode # From factory code
length: 3
- type: SEQUENCE
length: 6 # 6-digit sequence
# Result: EQ-BJ1-000001, EQ-SH2-000001
Decision Matrix:
Distributed-Friendly
Low ◄──────────────► High
│ │
High │ Sequence │ UUID
Perf │ │ (fast single-node)│ (distributed)
│ │ │
│ │ │
Low │ Natural Key │ Composite
│ (needs stability) │ (multi-tenant)
│ │
└────────────────────┘
coomia-dip recommends: Semantic ID (balances readability + distribution)
#4.3 Primary Key Configuration Examples
from ontology_sdk import ObjectTypeSpec, PrimaryKeyConfig
# Strategy 1: UUID (default, safest)
order = ObjectTypeSpec(
name="Order",
primary_key=PrimaryKeyConfig(
property="orderId",
strategy="UUID_V4"
)
)
# Strategy 2: Semantic ID (recommended)
equipment = ObjectTypeSpec(
name="Equipment",
primary_key=PrimaryKeyConfig(
property="equipmentId",
strategy="SEMANTIC_ID",
prefix="EQ",
segments=[
{"type": "PROPERTY", "source": "factoryCode", "length": 3},
{"type": "SEQUENCE", "length": 6}
]
)
)
# Strategy 3: Composite key (multi-tenant scenarios)
tenant_resource = ObjectTypeSpec(
name="TenantResource",
primary_key=PrimaryKeyConfig(
properties=["tenantId", "resourceId"],
strategy="COMPOSITE"
)
)
# Strategy 4: Natural key (stable business identifiers)
country = ObjectTypeSpec(
name="Country",
primary_key=PrimaryKeyConfig(
property="isoCode",
strategy="NATURAL",
immutable=True
)
)
#5. Advanced Property Features
#5.1 Derived Properties
properties:
# Expression-derived
fullName:
type: STRING
derived: true
expression: "concat(firstName, ' ', lastName)"
# Aggregation-derived (cross-relation)
totalOrderAmount:
type: DECIMAL
derived: true
aggregation:
type: SUM
relation: CustomerPlacedOrder
property: amount
# Conditional-derived
riskLevel:
type: ENUM
derived: true
expression: |
CASE
WHEN creditScore >= 750 THEN 'LOW'
WHEN creditScore >= 600 THEN 'MEDIUM'
ELSE 'HIGH'
END
#5.2 Audit Properties
spec:
audit:
enabled: true
properties:
createdBy:
type: STRING
autoPopulate: "$currentUser"
createdAt:
type: TIMESTAMP
autoPopulate: "$now"
updatedBy:
type: STRING
autoPopulate: "$currentUser"
updatedAt:
type: TIMESTAMP
autoPopulate: "$now"
version:
type: INTEGER
autoIncrement: true # Optimistic lock version
#5.3 Sensitive Properties
properties:
ssn:
type: STRING
sensitive: true
masking:
strategy: PARTIAL # Partial masking
visibleChars: 4 # Show last 4 characters
maskChar: "*"
# Display: ***-**-1234
encryption:
algorithm: AES_256_GCM
keyId: "key-ssn-001"
access:
requiredPermission: "view:pii"
#6. Type System Runtime Behavior
#6.1 Type Conversion Rules
┌─────────────────────────────────────────────────┐
│ Safe Type Conversion Matrix │
├──────────┬──────────────────────────────────────┤
│ Source │ Can safely convert to │
├──────────┼──────────────────────────────────────┤
│ INTEGER │ LONG, DOUBLE, DECIMAL, STRING │
│ LONG │ DOUBLE, DECIMAL, STRING │
│ DOUBLE │ STRING │
│ DECIMAL │ STRING │
│ BOOLEAN │ STRING, INTEGER(0/1) │
│ DATE │ TIMESTAMP, STRING │
│ STRING │ (requires explicit parse, no auto) │
└──────────┴──────────────────────────────────────┘
Safe conversion = no information loss
Unsafe conversions require explicit CAST
#6.2 NULL Value Handling
# coomia-dip NULL semantics
properties:
middleName:
type: STRING
required: false # NULL allowed
nullSemantics: ABSENT # NULL = value doesn't exist (default)
deletedAt:
type: TIMESTAMP
required: false
nullSemantics: NOT_APPLICABLE # NULL = not applicable (not deleted)
score:
type: DOUBLE
required: false
nullSemantics: UNKNOWN # NULL = value unknown
defaultOnNull: 0.0 # Treat NULL as 0.0 in queries
#7. Practical Example: Designing a Complete ObjectType
apiVersion: ontology/v1
kind: ObjectType
metadata:
name: Product
namespace: ecommerce
version: "1.0.0"
spec:
displayName: "Product"
description: "E-commerce platform product entity"
primaryKey:
property: productId
strategy: SEMANTIC_ID
prefix: "PRD"
properties:
productId:
type: STRING
required: true
immutable: true
name:
type: STRING
required: true
constraints:
minLength: 1
maxLength: 500
searchable: true # Full-text search support
sku:
type: STRING
required: true
unique: true
constraints:
pattern: "^[A-Z]{2}-\\d{6}$"
category:
type: ENUM
enumValues: [ELECTRONICS, CLOTHING, FOOD, BOOKS, OTHER]
required: true
price:
type: DECIMAL
required: true
constraints:
precision: 10
scale: 2
min: 0.01
stock:
type: INTEGER
required: true
constraints:
min: 0
defaultValue: 0
images:
type: ARRAY
itemType: ATTACHMENT
constraints:
maxItems: 10
maxFileSize: "5MB"
allowedMimeTypes: ["image/jpeg", "image/png", "image/webp"]
specifications:
type: MAP
keyType: STRING
valueType: STRING
constraints:
maxEntries: 30
dimensions:
type: STRUCT
structType: PhysicalDimension
# Derived properties
avgRating:
type: DOUBLE
derived: true
aggregation:
type: AVG
relation: ProductHasReview
property: rating
totalSales:
type: INTEGER
derived: true
aggregation:
type: SUM
relation: ProductInOrderItem
property: quantity
isLowStock:
type: BOOLEAN
derived: true
expression: "stock < 10 AND stock > 0"
displayLabel:
type: EXPRESSION
expression: "concat(name, ' (', sku, ')')"
constraints:
- name: pricePositive
expression: "price > 0"
message: "Product price must be greater than zero"
audit:
enabled: true
interfaces:
- Searchable
- Auditable
- SoftDeletable
lifecycle: DRAFT
#8. Property Type Selection Decision Tree
What type of data do you need to store?
│
├── Single value
│ ├── Text → STRING
│ ├── Integer → INTEGER (< 2^31) or LONG
│ ├── Decimal
│ │ ├── Exact calculation (money) → DECIMAL
│ │ └── Approximate OK (scientific) → DOUBLE
│ ├── Yes/No → BOOLEAN
│ ├── Time
│ │ ├── Date only → DATE
│ │ ├── Time only → TIME
│ │ ├── Date+Time → TIMESTAMP
│ │ └── Duration → DURATION
│ ├── Finite options → ENUM
│ └── Reference to another entity → REFERENCE
│
├── Structured value
│ ├── Fixed structure → STRUCT
│ ├── List → ARRAY
│ └── Key-value pairs → MAP
│
├── Special value
│ ├── Geographic location → GEO_POINT / GEO_SHAPE
│ ├── Vector embedding → VECTOR
│ ├── Time series data → TIMESERIES
│ ├── Files → ATTACHMENT
│ └── Computed expression → EXPRESSION
│
└── Computed from other properties → Any type + derived: true
#9. Performance Considerations
#9.1 Indexing Strategies
spec:
indexes:
# Single-field index
- name: idx_product_sku
properties: [sku]
type: UNIQUE
# Composite index
- name: idx_product_category_price
properties: [category, price]
type: BTREE
# Full-text search index
- name: idx_product_name_fts
properties: [name]
type: FULLTEXT
analyzer: "standard"
# Vector index
- name: idx_product_embedding
properties: [embedding]
type: HNSW
config:
m: 16
efConstruction: 200
#9.2 Storage Optimization Recommendations
| Scenario | Recommendation | Reason |
|---|---|---|
| High-cardinality STRING | Set maxLength | Avoid VARCHAR(MAX) |
| Frequently queried ENUM | Index + materialize | Avoid full table scan |
| Large ARRAY | Consider separate ObjectType | Avoid document bloat |
| Large MAP | Consider separate ObjectType | Avoid key explosion |
| ATTACHMENT | Separate storage (MinIO) | Avoid DB bloat |
| TIMESERIES | Dedicated time-series table | Write optimization |
#10. Common Mistakes and Best Practices
#Mistake 1: Overusing STRING
# Wrong
status:
type: STRING # Can store anything
# Correct
status:
type: ENUM
enumValues: [ACTIVE, INACTIVE, SUSPENDED]
#Mistake 2: Ignoring Precision Needs
# Wrong: Using DOUBLE for money has floating-point precision issues
amount:
type: DOUBLE
# Correct: Use DECIMAL for money
amount:
type: DECIMAL
constraints:
precision: 10
scale: 2
#Mistake 3: Excessive Nesting
# Wrong: 3-level nesting, hard to query
address:
type: STRUCT
structType: Address # Address nests City, City nests Country
# Correct: Flatten + reference
city:
type: REFERENCE
targetType: City
address:
type: STRUCT
structType: SimpleAddress # Only contains street, zipCode
#Mistake 4: Poor Primary Key Choice
# Wrong: Email as PK (may change)
primaryKey: email
# Wrong: Pure auto-increment (not distributed)
primaryKey:
strategy: SEQUENCE
# Correct: Semantic ID
primaryKey:
property: customerId
strategy: SEMANTIC_ID
prefix: "CUS"
#Key Takeaways
-
Choosing the right property type is the first step in Schema design. 20+ types cover everything from simple scalars to geospatial and vector embeddings. Prefer the most precise type (ENUM over STRING, DECIMAL over DOUBLE) and let the type system validate for you.
-
Constraints are the first line of defense for data quality. The three-layer constraint system (field → object → relation) pushes business rules down to the Schema layer, avoiding the classic "inconsistent application-layer validation" problem.
-
Primary key design has far-reaching impact. The Semantic ID approach balances readability, distributed-friendliness, and type identification — it's coomia-dip's recommended strategy. Avoid using mutable business fields as primary keys.
#Next Article
S4-03: ObjectType Lifecycle: The State Machine from DRAFT to ARCHIVED — We'll dive into ObjectType's 4 lifecycle states, transition rules, and compatibility checks.
tags: object-type, property-types, constraints, primary-key, schema-design, coomia-dip, data-modeling, validation