Table of Contents

JSON adapters

RuleWright's core doesn't depend on any JSON library. An adapter reads rule documents for the engine, and turns JSON facts into dictionaries. There is one for System.Text.Json and one for Newtonsoft.Json, and they are tested against each other.

Package Reader Facts helper
RuleWright.Json.SystemText (in the RuleWright package) SystemTextJsonReader SystemTextJsonFacts.ToDictionary(JsonElement)
RuleWright.Json.NewtonsoftJson NewtonsoftJsonReader NewtonsoftJsonFacts.ToDictionary(JToken)

Newtonsoft.Json

dotnet add package RuleWright.Execution
dotnet add package RuleWright.Json.NewtonsoftJson
using Newtonsoft.Json.Linq;
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Json.NewtonsoftJson;

RuleWrightEngine engine = new RuleWrightBuilder()
    .UseJsonReader(new NewtonsoftJsonReader())
    .Build();

LoadedRuleSet rules = engine.LoadRuleSet(File.ReadAllText("first-rule.json"));

JToken body = JToken.Parse("""{ "Customer": { "Age": 30 }, "Order": { "Total": 150 } }""");
Dictionary<string, object?> fact = NewtonsoftJsonFacts.ToDictionary(body);

RuleEvaluationResult result = engine.Evaluate(rules, fact);
Console.WriteLine($"Newtonsoft:       DiscountPercent = {result.Outputs["DiscountPercent"]} ({result.Outputs["DiscountPercent"]!.GetType().Name})");

System.Text.Json, for comparison

using System.Text.Json;
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Json.SystemText;

RuleWrightEngine stj = new RuleWrightBuilder()
    .UseJsonReader(new SystemTextJsonReader())
    .Build();

LoadedRuleSet same = stj.LoadRuleSet(File.ReadAllText("first-rule.json"));

using JsonDocument document = JsonDocument.Parse("""{ "Customer": { "Age": 30 }, "Order": { "Total": 150 } }""");
RuleEvaluationResult other = stj.Evaluate(same, SystemTextJsonFacts.ToDictionary(document.RootElement));
Console.WriteLine($"System.Text.Json: DiscountPercent = {other.Outputs["DiscountPercent"]} ({other.Outputs["DiscountPercent"]!.GetType().Name})");

Output

Newtonsoft:       DiscountPercent = 10 (Int64)
System.Text.Json: DiscountPercent = 10 (Int64)
Comments: True / True

Swapping the reader is the only change. Numbers are read the same way by both: whole numbers as long, other exact numbers as decimal, anything else as double, and a number too large to represent is a RuleParseException.

Matching keys without regard to case

A rule's field path matches a typed fact's members without regard to case, but a dictionary fact's keys through the dictionary's own comparer, which is exact by default. JSON from web clients is usually camelCase, so pass a key comparer, such as StringComparer.OrdinalIgnoreCase, as the second argument of SystemTextJsonFacts.ToDictionary(element, keyComparer) or NewtonsoftJsonFacts.ToDictionary(token, keyComparer).

The comparer applies to every object in the payload, including objects inside arrays. Two properties of one object that it treats as the same key ("Total" and "total") throw ArgumentException, so an ambiguous payload is refused rather than silently resolved. Typed and dictionary facts runs it.

Comments and trailing commas

Both readers accept // and /* */ comments and trailing commas, so a rule file can explain itself:

using System.Text.Json;
using Newtonsoft.Json.Linq;
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Extensions.Functions;
using RuleWright.Json.NewtonsoftJson;
using RuleWright.Json.SystemText;

// Both readers accept comments and trailing commas, so rule files can explain themselves.
string commented = """
    {
      // Retired in the 2026 price review; kept for the audit trail.
      "id": "old-rule",
      "enabled": false,
      "condition": { "field": "Order.Total", "operator": "GreaterThan", "value": 0, },
    }
    """;
Console.WriteLine($"Comments: {engine.Validate(commented).IsValid} / {stj.Validate(commented).IsValid}");

Nesting depth

Both adapters cap nesting at 64 levels, the JSON libraries' own default, so a deeply nested document can't exhaust the stack while it is parsed.

Writing your own adapter

IRuleJsonReader has one method, RuleJsonValue Read(string json), which turns text into RuleWright's small neutral JSON tree. That is the whole job of an adapter: parsing, validation and everything else happen in RuleWright.Serialization, shared by every adapter.

Next

Build rules in C#: skip JSON entirely.