Skip to content

Storing JSON

Tom Laird-McConnell edited this page May 24, 2026 · 1 revision

Storing JSON

LottaDB lets you store JSON documents without any configuration. Just save it -- LottaDB handles the rest. All top-level properties are automatically queryable and searchable, just like POCOs.

Zero-configuration: Just save JSON

No schema definition needed. All top-level simple-type properties are automatically:

  • Stored as full-fidelity JSON in table storage
  • Indexed in Lucene for search
  • Queryable via JsonExpression predicates

Save

var doc = JsonDocument.Parse("""{ "Name": "Alice", "Age": 30, "City": "Seattle" }""");
await db.SaveAsync(doc);

var key = doc.GetKey();    // auto-generated ULID
var etag = doc.GetETag();  // for optimistic concurrency

Get

var doc = await db.GetAsync(key);
doc.RootElement.GetProperty("Name").GetString();  // "Alice"

Delete

await db.DeleteAsync(key);

Search

Use JsonExpression predicates to query by property:

// By property value
var results = db.Search(je => je["Name"] == "Alice").ToList();

// Range query
var results = db.Search(je => je["Age"] > 20 && je["Age"] <= 30).ToList();

// By schema type (when using discriminators)
var people = db.Search(je => je.GetSchema() == "Person").ToList();

GetMany

// All JSON documents
await foreach (var doc in db.GetManyAsync(je => true)) { ... }

// With filter
await foreach (var doc in db.GetManyAsync(je => je["Age"] > 20)) { ... }

DeleteMany

// By predicate
await db.DeleteManyAsync(je => je["Age"] > 50);

// By schema type
await db.DeleteManyAsync(je => je.GetSchema() == "Person");

Batch save

var results = await db.SaveManyAsync(new object[]
{
    JsonDocument.Parse("""{ "Name": "Alice", "Age": 30 }"""),
    JsonDocument.Parse("""{ "Name": "Bob", "Age": 25 }"""),
});

Writes are batched transactionally (auto-flushed at 100 ops).

JsonExpression query syntax

JSON documents are queried using Expression> predicates. The JsonExpression class provides:

Expression Description
je["Name"] == "Alice" Property equality
je["Age"] > 20 Numeric comparison
je["Age"] > 20 && je["City"] == "Seattle" Logical AND
je["Active"] == true Boolean comparison
je.GetSchema() == "Person" Filter by schema type
je.GetKey() == "some-key" Filter by key

Fine-tuning with JsonSchema

When you need more control over indexing, key extraction, or nested property access, define a JsonSchema:

await db.SaveAsync(new JsonSchema
{
    Name = "Person",
    Properties = new()
    {
        new() { Name = "Name", Type = "string" },
        new() { Name = "Age", Type = "integer" },
    }
});

JsonSchema entities are stored as objects in the database. On startup, all JsonSchema entities are loaded from Table Storage and their mappers initialized automatically.

QueryableProperty

Each property defines a field to extract, index, and make queryable:

Field Description Default
Name Lucene field name + Table Storage column name Required
Type "string", "integer", "number", or "boolean" "string"
JsonPath JSONPath for nested values (e.g. "$.address.city") Property name

Key

The Key field specifies how to extract the document key:

new JsonSchema { Name = "Person", Key = "id" }           // top-level property
new JsonSchema { Name = "Person", Key = "$.user.id" }    // nested via JSONPath

KeyMode.Auto (default) generates a ULID when the key is missing. KeyMode.Manual requires the caller to provide it.

Discriminator (auto-classification)

The Match property lets LottaDB automatically classify JSON documents on save -- no need to tag documents manually. When a document matches a discriminator expression, that schema's indexing rules apply automatically:

await db.SaveAsync(new JsonSchema
{
    Name = "Person",
    Match = "$.type == 'person'",   // auto-classify when type == "person"
    Properties = new()
    {
        new() { Name = "Name", Type = "string" },
        new() { Name = "Age", Type = "integer" },
    }
});

await db.SaveAsync(new JsonSchema
{
    Name = "Company",
    Match = "$.type == 'company'",  // auto-classify when type == "company"
    Properties = new()
    {
        new() { Name = "Name", Type = "string" },
        new() { Name = "Industry", Type = "string" },
    }
});

Now documents are auto-classified on save based on their content:

// These are auto-classified as "Person" and "Company" based on $.type
await db.SaveAsync(JsonDocument.Parse("""{ "type": "person", "Name": "Alice", "Age": 30 }"""));
await db.SaveAsync(JsonDocument.Parse("""{ "type": "company", "Name": "Contoso", "Industry": "Tech" }"""));

// Search by schema type
var people = db.Search(je => je.GetSchema() == "Person").ToList();
var companies = db.Search(je => je.GetSchema() == "Company").ToList();

The discriminator uses JSONPath equality syntax:

  • "$.type == 'person'" -- match when type equals "person"
  • "$.category != 'draft'" -- match when category is not "draft"

Resolution order when saving a JsonDocument:

  1. Explicit doc.SetSchema("Person") -- if set, uses that schema
  2. Discriminator matching -- first JsonSchema whose Match expression matches
  3. Default schema -- auto-queryable, no specific type

Managing schemas

JsonSchema is a regular entity -- manage it with standard CRUD:

// List all schemas
await foreach (var schema in db.GetManyAsync<JsonSchema>()) { ... }

// Update -- adding a property triggers automatic reindex
await db.SaveAsync(new JsonSchema
{
    Name = "Person",
    Properties = new()
    {
        new() { Name = "Name", Type = "string" },
        new() { Name = "Age", Type = "integer" },
        new() { Name = "Email", Type = "string" },  // new!
    }
});

// Delete -- removes mapper and Lucene index entries
await db.DeleteAsync<JsonSchema>("Person");

The built-in On handler detects property changes and triggers a selective reindex -- only the affected type's Lucene entries are rebuilt.

Compared to POCOs

Storing POCOs Storing JSON
Definition C# class (optional attributes) JSON (optional JsonSchema)
Data type POCO (T) JsonDocument
Registration config.Store() None (or db.SaveAsync(new JsonSchema {...}))
Auto-queryable All simple-type properties All top-level simple-type properties
Save SaveAsync(entity) SaveAsync(doc)
Get GetAsync(key) GetAsync(key)
GetMany GetManyAsync(predicate?) GetManyAsync(je => ...)
Search Search(query?) Search(je => ...)
Delete DeleteAsync(key) DeleteAsync(key)
Query syntax LINQ predicates + query strings JsonExpression predicates
Nested properties Reflection on CLR properties JsonPath (e.g. $.address.city)
Polymorphism Base/derived types Discriminator matching
Vector search Supported Supported
Schema changes Rebuild on startup Selective reindex at runtime

Clone this wiki locally