Entity Engine & JSON
The JSON provider turns JSON arrays and members into statically typed entities. It validates
JSONPath mappings and identities, reconstructs mapped relationships, and evaluates KQL through a
read-only JSONRepository<T, TId> over one atomic file generation.
| Capability | JSON provider behavior |
|---|---|
| Access mode | Read-only typed repository. |
| Consistency | Each execution parses one complete file generation. |
| Identity | Provider-neutral key metadata bound to a JSON field path. |
| Queries | Provider-neutral KQL over mapped entities. |
| Relationships | Nested values or identities on owning endpoints, plus inverse reconstruction. |
| Mutation | Not supported by the current JSON repository. |
{
"users": [
{
"id": 1,
"login": "alice",
"connectionNumber": 4,
"active": true
},
{
"id": 2,
"login": "bob",
"connectionNumber": 14,
"active": true
}
]
}
The root repository maps one selected array. Entity properties then use paths relative to each selected item.
import klyn.data.json
import klyn.data.mapping
@Entity
@JSONCollection(path="$.users[*]")
class User:
@Id
@JSONField(path="$.id")
public property id as Int = 0
@JSONField(path="$.login")
public property login as String
@JSONField(path="$.connectionNumber")
public property connectionNumber as Int = 0
@JSONField(path="$.active")
public property active as Boolean = false
public User():
pass
| Annotation | Purpose |
|---|---|
@JSONCollection | Absolute RFC 9535 JSONPath selecting the entity items. |
@JSONField | JSONPath relative to the current entity item for a scalar or owning relationship. |
@Id | Provider-neutral identity whose value is read through its @JSONField. |
Every path and scalar conversion is checked before a value reaches application code. Missing required fields, incompatible property types, null keys, and duplicate identities raise explicit Entity Engine exceptions.
import klyn.data
import klyn.data.json
configuration = JSONConfiguration("data/users.json")
users = JSONRepository<User, Int>.open(configuration)
assert users.providerName() == "json"
assert users.count() == 2l
JSONConfiguration accepts a string path or a Path. The repository generic
key type must exactly match the mapped identity type. No file handle is retained between query
executions.
Direct document repositories use the explicit source form of KQL. The alias is mandatory and every referenced field remains statically typed.
frequentUsers = query:
from users user
where user.active and user.connectionNumber > 10
order by user.connectionNumber desc, user.login
result = frequentUsers.toList()
for user in result:
print(user.login)
Query creation remains lazy. The document is read when a terminal operation such as
toList(), single(), count(), or stream() executes.
users.validateBeforeRead()
bob = (query:
from users user
where user.id == 2
).single()
validateBeforeRead() validates the complete entity generation before the caller
consumes an item. Duplicate keys raise DuplicateIdentityException; absent or invalid
keys raise an entity mapping exception.
An owning relationship combines its cardinality annotation with @JSONField. The path
may select a nested entity, a target identity, or an array containing nested entities or
identities. An inverse endpoint declares mappedBy and no physical JSON path.
@ManyToOne(optional=false)
@JSONField(path="$.customerId")
public property customer as Customer
@OneToMany(mappedBy="customer")
public property orders as List<Order> = []
Scalar and identity materialization completes before relationships are fixed up. Eager relationships are populated automatically; a KQL fetch path can request a lazy relationship for the current execution.
Register an IndexDescriptor<T> when repeated equality filters target the same logical
property. The descriptor is statically tied to the entity type, validates its property path when
configuration is built, and stores type-aware index keys in a sidecar file.
configuration = JSONConfiguration(
"data/users.json",
indexes=[IndexDescriptor<User>("login", unique=true)]
)
users = JSONRepository<User, Int>.open(configuration)
Index metadata is rebuilt for the exact atomic document generation. It can reduce QIR row
evaluation, but JSON parsing remains a complete buffered read. Use query.explain() to
verify whether a plan selected an index; unique=true also rejects the first duplicate
indexed value.
- Writers should publish a complete temporary file through atomic replacement.
- One query execution never mixes bytes from the old and new file generations.
- Separate executions may legitimately observe separate generations.
- Use
validateBeforeRead()when complete validation must precede consumption. - Strict validation pins that immutable generation for subsequent non-fetch executions on the repository.
- Bound document size, depth, and result cardinality for untrusted JSON sources.
Continue with Entity Engine in Memory for typed KQL over existing Klyn collections.