Jsontron 0.1.1

dotnet add package Jsontron --version 0.1.1
                    
NuGet\Install-Package Jsontron -Version 0.1.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Jsontron" Version="0.1.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Jsontron" Version="0.1.1" />
                    
Directory.Packages.props
<PackageReference Include="Jsontron" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Jsontron --version 0.1.1
                    
#r "nuget: Jsontron, 0.1.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Jsontron@0.1.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Jsontron&version=0.1.1
                    
Install as a Cake Addin
#tool nuget:?package=Jsontron&version=0.1.1
                    
Install as a Cake Tool

EULA OSS GitHub

Overview

JSON Schema is excellent at shape: types, required fields, formats, enums. It is awkward at business rules that cross fields, compare against policy at the document root, or select a subset of nodes and assert something about each.

Jsontron adds that missing layer on top of JsonSchema.Net: Schematron-style rules / assert keywords whose context and test expressions are jq, evaluated by Devlooped.JQSharp.

  • Structural validation stays with JSON Schema.
  • Cross-cutting rules stay in the schema, next to the data they describe.
  • jq expressions are parsed once when the schema is built, then reused.

Quick start

using System.Text.Json;
using Json.Schema;
using Jsontron;

// Once per process — extends Dialect.Default with rules/assert
MetaSchemas.Register();

var schema = JsonSchema.FromText(
    """
    {
      "type": "object",
      "required": [ "orders", "policy", "approvedCustomers" ],
      "rules": [
        {
          "context": ".orders[] | select(.total > 1000)",
          "asserts": [
            {
              "test": ".discount >= ($root.policy.minHighValueDiscount // 0.1)",
              "message": "High-value order \\(.id // \"?\") discount \\(.discount) is below policy"
            },
            {
              "test": ".customer.id | IN($root.approvedCustomers[])",
              "message": "Customer \\(.customer.id) is not on the approved list"
            }
          ]
        }
      ]
    }
    """);

using var doc = JsonDocument.Parse(
    """
    {
      "policy": { "minHighValueDiscount": 0.15 },
      "approvedCustomers": [ "acme", "globex" ],
      "orders": [
        {
          "id": "O-1001",
          "total": 2500,
          "discount": 0.05,
          "customer": { "id": "initech" }
        },
        {
          "id": "O-1002",
          "total": 80,
          "discount": 0,
          "customer": { "id": "unknown" }
        }
      ]
    }
    """);

var result = schema.Evaluate(
    doc.RootElement,
    new EvaluationOptions { OutputFormat = OutputFormat.List });

// O-1001 fails both asserts (discount + customer).
// O-1002 is ignored by the rule (total ≤ 1000).
Console.WriteLine(result.IsValid); // False

Why jq?

jq already knows how to walk JSON: filter arrays, default missing fields, compare values, and build messages with string interpolation. Jsontron reuses that instead of inventing another expression language.

Idea In Jsontron
Select nodes to check context — jq filter over the current instance
Predicate that must hold asserts[].test — jq filter with . = each context node
Human-readable failure asserts[].message — jq expression → string
Document root $root (always bound for context, test, and message)

An assert fails when the test is not jq-truthy (false, null, or an empty stream) — the same idea as Schematron assert.

Keywords

rules

Full form: an array of rules. Each rule picks context nodes, then runs one or more asserts.

{
  "rules": [
    {
      "context": ".lineItems[] | select(.sku | startswith(\"PROMO-\"))",
      "asserts": [
        {
          "test": ".qty <= ($root.promotions[.sku].maxQty // 1)",
          "message": "Promo \\(.sku) allows at most \\($root.promotions[.sku].maxQty // 1), got \\(.qty)"
        }
      ]
    }
  ]
}

assert (sugar for context: ".")

When the rule is “about this node,” skip the full rules array:

{
  "type": "object",
  "properties": {
    "email": { "type": "string", "format": "email" },
    "age": { "type": "integer", "minimum": 0 }
  },
  "assert": ".age >= 18 or (.guardianEmail | type) == \"string\""
}

Forms accepted:

JSON Meaning
"assert": "<jq>" One assert; test and message source are that expression
"assert": { "test", "message" } Explicit message (still a jq string expression)
"assert": [ ... ] Several asserts, all on the same context: "." rule
{
  "assert": [
    ".status | IN([\"draft\", \"submitted\", \"approved\"])",
    {
      "test": ".status != \"approved\" or .approver != null",
      "message": "Approved documents require an approver"
    }
  ]
}

If both assert and rules appear on the same schema object, sugar asserts are merged into the rule with context: "." (created if needed) and evaluated once.

Nested schema locations

assert / rules apply to the instance at that schema location. Under properties / items, . is the nested value; $root remains the full document.

{
  "type": "object",
  "properties": {
    "shipments": {
      "type": "array",
      "items": {
        "type": "object",
        "required": [ "weightKg", "method" ],
        "assert": {
          "test": ".method != \"air\" or .weightKg <= $root.limits.maxAirKg",
          "message": "Air shipment \\(.id // \"?\") exceeds max air weight"
        }
      }
    }
  }
}

Messages

message is a jq expression that should produce a string. If it does not start with ", Jsontron wraps it as a jq string for you so plain text just works:

"message": "Discount too low"

Use jq interpolation when you want values in the text (escape backslashes in JSON):

"message": "Line \\(.sku): qty \\(.qty) exceeds cap"

Or start with " yourself for a full jq string expression.

Registration

MetaSchemas.Register();

That:

  1. Extends Dialect.Default (and BuildOptions.Default.Dialect) with rules / assert
  2. Registers the vocabulary and meta-schema
    (https://www.schemastore.org/jsontron-0.1.json)

After Register(), ordinary JsonSchema.FromText / Build calls pick up the keywords with no extra BuildOptions plumbing.

Keyword syntax is also published as schemas/jsontron-0.1.json.

Design notes

  • Build-time compile: invalid keyword shape or invalid jq fails when the schema is built, not on the first instance.
  • No rule shadowing (v1): every rule runs (closer to Schematron 2025 group than classic pattern shadowing).
  • Empty context: if context matches nothing, asserts do not run — vacuously valid.
  • Stack: JsonSchema.Net 9.x + Devlooped.JQSharp 1.0.2+ (evaluation-time variables for $root).

Open Source Maintenance Fee

To ensure the long-term sustainability of this project, users of this package who generate revenue must pay an Open Source Maintenance Fee. While the source code is freely available under the terms of the License, this package and other aspects of the project require adherence to the Maintenance Fee.

To pay the Maintenance Fee, become a Sponsor at the proper OSMF tier. A single fee covers all of Devlooped packages.

Sponsors

Clarius Org MFB Technologies, Inc. SandRock DRIVE.NET, Inc. Keith Pickford Thomas Bolon Kori Francis Reuben Swartz Jacob Foshee alternate text is missing from this package README image Eric Johnson Jonathan Ken Bonny Simon Cropp agileworks-eu Zheyu Shen Vezel ChilliCream 4OTC domischell Adrian Alonso torutek Ryan McCaffery Seika Logiciel Andrew Grant eska-gmbh Geodata AS

Sponsor this project

Learn more about GitHub Sponsors

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.1 89 8/11/2026