- Morphe - Application Data Modeling Specification
Morphe (KA:MO1) is a simple, human-readable base data modeling specification designed to facilitate consistent transpilation of models into multiple programming languages.
The name represents the ancient Greek "form" or "shape", implying an ideal form or prototype from which other forms are derived. This symbolizes how declaratively modelled data is generatively transformed into code across different formats and languages.
| Version | Status | Description | Docs |
|---|---|---|---|
| KA:MO1 | đźš§ In Progress | First stable release of the Morphe core specification | This document |
| KA:MO1:YAML1 | đźš§ In Progress | YAML format standard for KA:MO1 | format/YAML.md |
| KA:MO1:TS1 | đźš§ In Progress | TypeScript format standard for KA:MO1 | format/TS.md |
| KA:MO1:GO1 | đźš§ In Progress | Go format standard for KA:MO1 | format/GO.md |
| KA:MO1:PSQL1 | đźš§ In Progress | PostgreSQL format standard for KA:MO1 | format/PSQL.md |
The Morphe specification system uses a hierarchical naming scheme to clearly identify different types of specifications:
KA:- Organization/context prefixMO1- Base specification identifier (Morphe version 1)- Example:
KA:MO1- The core Morphe data modeling specification
- Extends base specification with format identifier
YAML1- Format representation identifier (YAML version 1)- Example:
KA:MO1:YAML1- The YAML format specification for Morphe
To rally ecosystem support around a canonical standard, specifications can define a "base format" that serves as the primary reference implementation. For Morphe, YAML (KA:MO1:YAML1) serves as the base format, providing:
- Canonical Reference: All examples and documentation use YAML as the primary format
- Ecosystem Alignment: Plugin developers can focus on YAML-first implementations
- Interoperability: A common format that all tools and implementations can understand
- Developer Experience: Consistent, human-readable syntax across the ecosystem
Other formats (Go, TypeScript, PostgreSQL) are considered transpilation targets from the base YAML format.
This specification uses YAML examples rather than pseudo-code for the following reasons:
Advantages of YAML Examples:
- Concrete Implementation: Provides actionable, copy-pasteable examples
- Canonical Reference: Serves as the authoritative syntax reference
- Developer Experience: Immediately usable examples reduce learning curve
- Consistency: Aligns with the base format used across the ecosystem
- Validation: Real syntax can be tested and validated
Trade-offs Considered:
- Format Coupling: Examples are tied to YAML syntax, but this is mitigated by YAML being the canonical base format
- Abstraction Level: Less abstract than pseudo-code, but the concrete examples provide more value for implementation
Since YAML serves as Morphe's base format, using YAML examples creates a cohesive developer experience while maintaining the specification's practical utility.
This hierarchical naming scheme provides several benefits:
- Clear separation between base concepts and format representations
- Support for multiple format representations of the same base specification
- Ability to transpile between different formats (e.g., YAML to Go, YAML to TypeScript)
- Version tracking at each level of the specification
- Extensibility for future formats
-
Base Specifications:
KA:MO1- Core Morphe data modeling specification
-
Format Specifications:
KA:MO1:YAML1- YAML format representationKA:MO1:GO1- Go format representationKA:MO1:TS1- TypeScript format representationKA:MO1:PSQL1- PostgreSQL format representation
-
đź“– Single Source of Truth
- Define your data models once in a structured format
- Automatic code generation across your stack
- Consistent type definitions across languages
- Reduced manual synchronization effort
-
🔄 Rich Type System
- Built-in primitive types (UUID, String, Integer, etc.)
- Custom enumeration support
- Relationship modeling (HasOne, HasMany, etc.)
- Support for protected and sealed fields
-
đź”§ Flexible Architecture
Modelsfor persisted data structuresStructuresfor reusable field groupsEntitiesfor business data aggregationEnumsfor constant value sets- Clear separation of concerns
-
📏 Standardized Versioning
- Core spec versioning (KAMO)
- Language-specific standards (KAMO-LANG)
- Backward compatibility guidelines
- Clear upgrade paths
The Morphe specification defines a comprehensive set of features that compile plugins can implement. Each plugin can document which features it supports, ensuring clarity about capabilities and limitations.
| Feature | Description | Spec Status |
|---|---|---|
| Models | Primary persisted data structures with fields, identifiers, and relationships | âś… Complete |
| Entities | Business data aggregation structures that reference model fields | âś… Complete |
| Enums | Predefined constant value sets with primitive types | âś… Complete |
| Structures | Standalone, non-persisted field groupings for DTOs | âś… Complete |
| Feature | Description | Spec Status |
|---|---|---|
| EnumFields | Using enums as field types in models, entities, and structures | âś… Complete |
| ModelRelationPolymorphism | Polymorphic relationships (HasOnePoly, HasManyPoly, ForOnePoly, ForManyPoly) in models | âś… Complete |
| EntityRelationPolymorphism | Polymorphic relationships in entities | âś… Complete |
| ModelRelationAliasing | Custom relationship naming and aliasing in models | âś… Complete |
| EntityRelationAliasing | Custom relationship naming and aliasing in entities | âś… Complete |
Compile plugins should document their feature support using this standardized list. For example:
# Plugin Feature Matrix
supported_features:
- Models
- Entities
- Enums
- ModelRelationPolymorphism
- EntityRelationPolymorphism
unsupported_features:
- ModelRelationAliasing
- EntityRelationAliasing
- EnumFields
- StructuresThis ensures users understand exactly what functionality is available when using specific plugins.
As your application's codebase grows, structural changes become increasingly expensive.
Let's take something as simple as adding a country field to addresses managed by your application. To achieve this across the stack, you typically need to manually update the following things:
addressSQL table creation scriptsaddressSQL queries- Backend type definitions, such as an
Addressclass - Associated automated test arrangement and assertion blocks
- API endpoint documentation
- Front-end type definitions, such as an
Addresstypescript type - Front-end form components
- ...
Tooling exists to reduce the manual work needed for some of these steps. Common examples might be an ORM that generates SQL scripts automatically from backend types or GraphQL typescript type generators from GraphQL query files. But these tools are fragmented and tend to leverage multiple disparate "sources of truth" across your application.
The Morphe specification aims to provide a simple schema for automatically updating your code artifacts across technologies, eliminating human error, and accelerating development velocity.
By consolidating your application's data structures into a single source of truth for your entire stack, you only have to touch one place instead of many and let your selected plugins handle the rest.
Atomic field types are type primitives that represent a single, indivisible unit of data required for defining fields on higher-order data structures.
UUID: A RFC-4122 compatible UUID stringAutoIncrement: A classic numeric, auto-incrementable record IDString: A variable-length stringInteger: A numeric value for zero, whole numbers, and their negative counterpartsFloat: A numeric floating-point decimal valueBoolean: A boolean (true / false) valueTime: A timestamp value with a UTC offsetDate: A timestamp value as a date with a UTC offset and zero time valuesProtected: An encryptable and decryptable value for sensitive information such as API keysSealed: A hashable value that can not be decrypted (typically passwords)
Enums can be used as field types within models, entities, and structures by referencing their name. When used as a field type, the enum enforces that only predefined values from the enum's entries can be assigned to that field.
Example: Using Nationality enum as a field type
name: Person
fields:
ID:
type: AutoIncrement
FirstName:
type: String
LastName:
type: String
Nationality:
type: Nationality # References the Nationality enumSee the Enums section for complete enum definition syntax and examples.
Structures represent standalone, reusable, non-persisted groupings of fields, for more flexible use inside of application projects, for example as subtypes or DTOs (data-transfer-objects).
Each structure consists of a name, and a set of fields.
Note: Unlike models, structures do not have relationships or identifiers, as they are not persisted.
Example: address.str
name: Address
fields:
Street:
type: String
HouseNr:
type: String
ZipCode:
type: String
City:
type: StringEnums are predefined sets of constant values that can be used as types for fields within models, entities, and structures. They enforce consistency by limiting possible values to a fixed set.
Each enum consists of a name, a primitive type (String, Integer, Float), and a set of entries. Entries are defined as symbol names with an associated primitive representation.
Example: nationality.enum
name: Nationality
type: String
entries:
US: 'American'
DE: 'German'
FR: 'French'Example: universal-number.enum
name: UniversalNumber
type: Float
entries:
Pi: 3.1415926535
Euler: 2.7182818285Enums can be used in three ways:
- As Field Types: Reference the enum name as a field type (see Enumeration Field Types)
- Data Validation: Ensure only valid values are assigned to fields
- Code Generation: Generate constants, types, or lookup tables in target languages
Models are the primary persisted data structures. They are somewhat analogous to relational SQL tables.
Each model consists of a name, a set of fields, a set of identifiers, and related models.
Example: person.mod
name: Person
fields:
ID:
type: AutoIncrement
attributes:
- mandatory
FirstName:
type: String
LastName:
type: String
Nationality:
type: Nationality
identifiers:
primary: ID
name:
- FirstName
- LastName
related:
ContactInfo:
type: HasOne
Company:
type: ForOneDenoted by the fields: key, the model fields are specified as a uniquely named key and the finer-grained field configuration.
Each field has exactly one type.
Each field may have a list of unconstrained, lower, snake-case attributes. Attributes have no inherent meaning to the Morphe specification but may be required by specific transpiling implementations.
Denoted by the identifiers: key, this section always requires a primary: identifier with a corresponding field name.
Composite or secondary identifier groups can be added with a unique key (i.e. addr:), and a list of relevant fields.
Denoted by the related: key, this section lists all related models. This means both dependencies and dependents (prerequisites).
Each relationship is characterized by an ownership and a cardinality that differentiates between 1 and n related entities.
For: The current model is "for" related models, when the current model is a dependent on the existence of related models.- Example: "Address ForOne User": Address is reliant on the existence of a specified User, because Address explicitly references User identifiers.
Has: The current model "has" related models, when the current model is required by the related models.- Example: "Address HasMany Document": Address supports the reliance of Documents on the existence of specified Addresses, because Documents explicitly reference Address identifiers.
One: Relationships have a cardinality of one, when the current model's relationship supports one related model instance.- Example: "Address ForOne User": Address is related to one user.
Many: Relationships have a cardinality of many, when the current model's relationship supports a variable number of related model instances.- Example: "Owner HasMany Address": Owner is related to many addresses.
Polymorphic relationships allow models to be related to multiple different model types through a single association. Morphe supports four types of polymorphic relationships:
These relationships indicate that the current model "has" related models through a polymorphic interface.
HasOnePoly: One-to-one polymorphic relationshipHasManyPoly: One-to-many polymorphic relationshipthrough: Specifies the polymorphic interface namealiased: (Optional) Specifies the actual target model name when different from the relationship name
Example: person.mod with polymorphic relationships
name: Person
fields:
ID:
type: AutoIncrement
attributes:
- mandatory
FirstName:
type: String
LastName:
type: String
identifiers:
primary: ID
related:
Comment:
type: HasOnePoly
through: Commentable
Tag:
type: HasManyPoly
through: TaggableThese relationships indicate that the current model belongs to one or many of the specified model types.
ForOnePoly: Many-to-one polymorphic relationshipForManyPoly: Many-to-many polymorphic relationshipfor: Specifies the list of possible target model types
Example: comment.mod with polymorphic relationship
name: Comment
fields:
ID:
type: AutoIncrement
attributes:
- mandatory
Content:
type: String
CreatedAt:
type: String
identifiers:
primary: ID
related:
Commentable:
type: ForOnePoly
for:
- Person
- CompanyExample: tag.mod with polymorphic relationship
name: Tag
fields:
ID:
type: AutoIncrement
attributes:
- mandatory
Name:
type: String
Color:
type: String
identifiers:
primary: ID
related:
Taggable:
type: ForManyPoly
for:
- Person
- CompanyMorphe supports relationship aliasing, which allows you to use custom names for relationships while referencing existing model types. This is particularly useful when a model needs multiple relationships to the same target model type.
Use the aliased property to specify the actual target model:
Example: person.mod with aliased relationships
name: Person
fields:
ID:
type: AutoIncrement
attributes:
- mandatory
Name:
type: String
identifiers:
primary: ID
related:
WorkContact:
type: ForOne
aliased: Contact # References the Contact model
PersonalContact:
type: ForOne
aliased: Contact # Same target model with different relationship name
WorkProject:
type: ForMany
aliased: Project
PersonalProject:
type: ForMany
aliased: ProjectLike regular relationships, polymorphic relationships support the aliased property to decouple the relationship name from the target model name. When aliased is not specified, the relationship name itself is used as the target model name.
Example: post.mod with polymorphic aliasing for semantic field naming
name: Post
fields:
ID:
type: AutoIncrement
Title:
type: String
identifiers:
primary: ID
related:
Note: # Semantic field name (used in generated code)
type: HasOnePoly
through: Commentable
aliased: Comment # Actual model type (Comment model must exist)
Comment: # Without aliased, this references a Comment model directly
type: HasManyPoly
through: CommentableThis pattern is particularly useful for creating meaningful field names in generated code while maintaining the polymorphic relationship structure.
Entities are indirect data structures that route internally to model field subsets for business data flattening and aggregation. They are analogous to SQL views, decoupling domain data from underlying technical data structures (Models).
Entities consist of a name, a set of fields, identifiers, and related entities. Identifiers and field types are inherited from models.
Example: person.ent
name: Person
fields:
ID:
type: Person.ID
attributes:
- immutable
- mandatory
LastName:
type: Person.LastName
Nationality:
type: Person.Nationality
Email:
type: Person.ContactInfo.Email
identifiers:
primary: ID
related:
Company:
type: ForOneDenoted by the fields: key, the entity fields are specified as a uniquely named key (example: Street:) and the finer-grained field configuration.
Each field may have a list of unconstrained, lower, snake-case (Example: - immutable, - mandatory) attributes. Attributes may be required by specific transpiling implementations, but have no inherent meaning to the Morphe specification itself.
Denoted by the identifiers: key, this section specifies how the entity can be uniquely identified. Entity identification strategies will be expanded in future versions to support various approaches like virtual/composite keys, hierarchical IDs, and surrogate bridging.
Currently, entity identification supports globally unique IDs from a single referenced model field.
Entity field types are indirected model field paths. The path begins with a root model (Example: type: User.Address.Street -> User as the "root model"), and includes related models, terminating in a field of the last related model name. The field inherits the type from the terminal model.
Denoted by the related: key, this section lists all related entities. This means both dependencies and dependents (prerequisites).
Each relationship is characterized by an ownership and a cardinality that differentiates between 1 and n related entities.
For: The current entity is dependent on related entities.- Example: "Address ForOne User": Address is reliant on the existence of a specified User, because Address explicitly references User identifiers.
Has: The current entity is required by related entities.- Example: "Address HasMany Document": Address supports the reliance of Documents on the existence of specified Addresses, because Documents explicitly reference Address identifiers.
One: The relationship supports one related entity instance.- Example: "Address ForOne User": Address is related to one user.
Many: The relationship supports multiple related entity instances.- Example: "Owner HasMany Address": Owner is related to many addresses.
Entities support the same polymorphic relationship types as models, allowing them to represent complex business relationships across multiple entity types. Like models, the aliased property is optional - when not specified, the relationship name is used as the target entity name.
Example: person.ent with polymorphic relationships
name: Person
fields:
ID:
type: Person.ID
attributes:
- immutable
- mandatory
LastName:
type: Person.LastName
Nationality:
type: Person.Nationality
identifiers:
primary: ID
related:
Comment:
type: HasOnePoly
through: Commentable
Tag:
type: HasManyPoly
through: TaggableExample: comment.ent with polymorphic relationship
name: Comment
fields:
ID:
type: Comment.ID
attributes:
- immutable
- mandatory
Content:
type: Comment.Content
CreatedAt:
type: Comment.CreatedAt
identifiers:
primary: ID
related:
Commentable:
type: ForOnePoly
for:
- Person
- CompanyExample: tag.ent with polymorphic relationship
name: Tag
fields:
ID:
type: Tag.ID
attributes:
- immutable
- mandatory
Name:
type: Tag.Name
Color:
type: Tag.Color
identifiers:
primary: ID
related:
Taggable:
type: ForManyPoly
for:
- Person
- CompanyEntities support the same aliasing patterns as models:
Example: person-profile.ent with aliased relationships
name: PersonProfile
fields:
ID:
type: Person.ID
attributes:
- immutable
- mandatory
Name:
type: Person.Name
WorkEmail:
type: Person.WorkContact.Email # Traversing through aliased relationship
PersonalPhone:
type: Person.PersonalContact.Phone
identifiers:
primary: ID
related:
PrimaryCompany:
type: ForOne
aliased: Company
SecondaryCompany:
type: ForOne
aliased: CompanyEntity fields can traverse through aliased model relationships using the relationship name (not the aliased target). The aliasing resolution happens transparently during compilation.
We welcome contributions to the Morphe specification and its format-specific implementations! Here's how you can help:
-
Specification Improvements
- Clarifying existing documentation
- Suggesting new features or field types
- Identifying gaps or inconsistencies
- Improving examples and use cases
-
Implementation Feedback
- Testing format-specific implementations
- Reporting bugs or unexpected behavior
- Suggesting optimizations for generated code
- Proposing new format implementations
-
Issues First
- Start by creating an issue to discuss your proposed changes
- For bugs, include the specification version (e.g., KA:MO1, KA:MO1:YAML1)
- For features, explain the use case and expected benefits
-
Pull Requests
- Fork the relevant repository
- Create a branch with a descriptive name
- Make focused, atomic commits with clear messages
- Reference the related issue in your PR description
-
Specification Changes
- Use clear, concise language
- Provide examples for new features
- Maintain consistent formatting with existing docs
- Follow format best practices in examples
-
Code Contributions
- Follow the format-specific style guide
- Include tests for new features
- Update documentation as needed
- Ensure backwards compatibility
Open a discussion in the relevant repository's issues section. We aim to respond within a few business days.
This project is licensed under the MIT License.