Databases & Entities JSON Read-only

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.

Provider Contract
CapabilityJSON provider behavior
Access modeRead-only typed repository.
ConsistencyEach execution parses one complete file generation.
IdentityProvider-neutral key metadata bound to a JSON field path.
QueriesProvider-neutral KQL over mapped entities.
RelationshipsNested values or identities on owning endpoints, plus inverse reconstruction.
MutationNot supported by the current JSON repository.
Source Document
{
  "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.

JSON Entity Mapping
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
AnnotationPurpose
@JSONCollectionAbsolute RFC 9535 JSONPath selecting the entity items.
@JSONFieldJSONPath relative to the current entity item for a scalar or owning relationship.
@IdProvider-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.

Open the Repository
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.

Typed JSON Query

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.

Identity Validation
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.

JSON Relationships

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.

Optional Persistent Indexes

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.

Atomic Reads
  • 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.
Next Step

Continue with Entity Engine in Memory for typed KQL over existing Klyn collections.