diff --git a/Hyperbee.Json.sln b/Hyperbee.Json.sln
index 39ba2a45..9db95102 100644
--- a/Hyperbee.Json.sln
+++ b/Hyperbee.Json.sln
@@ -34,6 +34,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Json.Tests", "test\Hyperbee.Json.Tests\Hyperbee.Json.Tests.csproj", "{97886205-1467-4EE6-B3DA-496CA3D086E4}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hyperbee.Json.Benchmark", "test\Hyperbee.Json.Benchmark\Hyperbee.Json.Benchmark.csproj", "{45C24D4B-4A0B-4FF1-AC66-38374D2455E9}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -48,6 +50,10 @@ Global
{97886205-1467-4EE6-B3DA-496CA3D086E4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{97886205-1467-4EE6-B3DA-496CA3D086E4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{97886205-1467-4EE6-B3DA-496CA3D086E4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {45C24D4B-4A0B-4FF1-AC66-38374D2455E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {45C24D4B-4A0B-4FF1-AC66-38374D2455E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {45C24D4B-4A0B-4FF1-AC66-38374D2455E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {45C24D4B-4A0B-4FF1-AC66-38374D2455E9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -56,6 +62,7 @@ Global
{1FA7CE2A-C9DA-4DC3-A242-5A7EAF8EE4FC} = {870D9301-BE3D-44EA-BF9C-FCC2E87FE4CD}
{4DBDB7F5-3F66-4572-80B5-3322449C77A4} = {1FA7CE2A-C9DA-4DC3-A242-5A7EAF8EE4FC}
{97886205-1467-4EE6-B3DA-496CA3D086E4} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0}
+ {45C24D4B-4A0B-4FF1-AC66-38374D2455E9} = {F9B24CD9-E06B-4834-84CB-8C29E5F10BE0}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {32874F5B-B467-4F28-A8E2-82C2536FB228}
diff --git a/Hyperbee.Json.sln.DotSettings b/Hyperbee.Json.sln.DotSettings
index d9cc86a7..3c37ed14 100644
--- a/Hyperbee.Json.sln.DotSettings
+++ b/Hyperbee.Json.sln.DotSettings
@@ -36,6 +36,11 @@
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="__" Suffix="" Style="aaBb" /></Policy>
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy>
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="__" Suffix="" Style="aaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy></Policy>
True
C:\Development\Hyperbee.Core\Hyperbee.Core.sln.DotSettings
@@ -45,6 +50,7 @@
True
True
True
+ True
True
True
True
diff --git a/README.md b/README.md
index 48e4d9c0..4d36399a 100644
--- a/README.md
+++ b/README.md
@@ -1,44 +1,42 @@
# JSONPath
-A C# implementation of JSONPath for .NET `System.Text.Json` with a plugable expression selector.
+JSON Path is a query language for JSON documents inspired by XPath. JSONPath defines
+a string syntax for selecting and extracting JSON values from within a given JSON document.
-## Why
+This library is a C# implementation of JSONPath for .NET `System.Text.Json` and `System.Text.Json.Nodes`.
-.NET `System.Text.Json` lacks support for JSONPath. The primary goal of this project is to create a JSONPath library for .NET that will
+The implementation
-* Directly leverage `System.Text.Json`
-* Align with the draft JSONPath Specification
+* Works natively with both `JsonDocument` (`JsonElement`) and `JsonNode`
+* Can be extended to support other JSON models
+* Aligns with the draft JSONPath Specification RFC 9535
* [Working Draft](https://github.com/ietf-wg-jsonpath/draft-ietf-jsonpath-base).
* [Editor Copy](https://ietf-wg-jsonpath.github.io/draft-ietf-jsonpath-base/draft-ietf-jsonpath-base.html)
-* Function according to the emerging consensus of use based on the majority of existing implementations; except through concious exception or deference to the RFC.
- * [Parser Comparison](https://cburgmer.github.io/json-path-comparison)
-* Provide a plugable model for expression script handling.
+* Functions according to the emerging consensus of use based on the majority of existing
+ implementations; except through concious exception or deference to the RFC.
+ * [Parser Comparison Results](https://cburgmer.github.io/json-path-comparison)
+ * [Parser Comparison GitHub](https://github.com/cburgmer/json-path-comparison/tree/master)
-## JSONPath Expressions
+## JSONPath Syntax
-JSONPath expressions always refers to a JSON structure in the same way as XPath
-expression are used in combination with an XML document. Since a JSON structure is
-usually anonymous and doesn't necessarily have a root member object JSONPath
-assumes the abstract name `$` assigned to the outer level object.
+JSONPath expressions refer to a JSON structure in the same way as XPath expressions
+are used in combination with an XML document. JSONPath assumes the name `$` is assigned
+to the root level object.
-JSONPath expressions can use the dot-notation:
+JSONPath expressions can use dot-notation:
$.store.book[0].title
-or the bracket-notation:
+or bracket-notation:
$['store']['book'][0]['title']
-for input paths. Internal or output paths will always be converted to the more
-general bracket-notation.
-
JSONPath allows the wildcard symbol `*` for member names and array indices. It
-borrows the descendant operator `..` from [E4X][e4x] and the array slice
+borrows the descendant operator `..` from [E4X][e4x], and the array slice
syntax proposal `[start:end:step]` from ECMASCRIPT 4.
-Expressions of the underlying scripting language (``) can be used as an
-alternative to explicit names or indices, as in:
+Expressions can be used as an alternative to explicit names or indices, as in:
$.store.book[(@.length-1)].title
@@ -54,15 +52,15 @@ syntax elements with its XPath counterparts:
|:----------|:-------------------|:-----------------------------------------------------------
| `/` | `$` | The root object/element
| `.` | `@` | The current object/element
-| `/` | `.` or `[]` | Child operator
+| `/` | `.` or `[]` | Child operator
| `..` | n/a | Parent operator
-| `//` | `..` | Recursive descent. JSONPath borrows this syntax from E4X.
-| `*` | `*` | Wildcard. All objects/elements regardless their names.
+| `//` | `..` | Recursive descent. JSONPath borrows this syntax from E4X.
+| `*` | `*` | Wildcard. All objects/elements regardless their names.
| `@` | n/a | Attribute access. JSON structures don't have attributes.
-| `[]` | `[]` | Subscript operator. XPath uses it to iterate over element collections and for [predicates][xpath-predicates]. In Javascript and JSON it is the native array operator.
-| `\|` | `[,]` | Union operator in XPath results in a combination of node sets. JSONPath allows alternate names or array indices as a set.
-| n/a | `[start:end:step]`| Array slice operator borrowed from ES4.
-| `[]` | `?()` | Applies a filter (script) expression.
+| `[]` | `[]` | Subscript operator. XPath uses it to iterate over element collections and for [predicates][xpath-predicates]. In Javascript and JSON it is the native array operator.
+| `\|` | `[,]` | Union operator in XPath results in a combination of node sets. JSONPath allows alternate names or array indices as a set.
+| n/a | `[start:end:step]` | Array slice operator borrowed from ES4.
+| `[]` | `?()` | Applies a filter (script) expression.
| n/a | `()` | Script expression, using the underlying script engine.
| `()` | n/a | Grouping in XPath
@@ -106,67 +104,20 @@ Given a simple JSON structure that represents a bookstore:
| XPath | JSONPath | Result | Notes
|:----------------------|:--------------------------|:---------------------------------------|:------
-|`/store/book/author` | `$.store.book[*].author` | The authors of all books in the store
-|`//author` | `$..author` | All authors
-|`/store/*` | `$.store.*` | All things in store, which are some books and a red bicycle
-|`/store//price` | `$.store..price` | The price of everything in the store
-|`//book[3]` | `$..book[2]` | The third book
-|`//book[last()]` | `$..book[(@.length-1)]
$..book[-1:]` | The last book in order
-|`//book[position()<3]`| `$..book[0,1]`
`$..book[:2]`| The first two books
+|`/store/book/author` | `$.store.book[*].author` | The authors of all books in the store
+|`//author` | `$..author` | All authors
+|`/store/*` | `$.store.*` | All things in store, which are some books and a red bicycle
+|`/store//price` | `$.store..price` | The price of everything in the store
+|`//book[3]` | `$..book[2]` | The third book
+|`//book[last()]` | `$..book[(@.length-1)]
$..book[-1:]` | The last book in order
+|`//book[position()<3]` | `$..book[0,1]`
`$..book[:2]`| The first two books
|`//book/*[self::category|self::author]` or `//book/(category,author)` in XPath 2.0 | `$..book[category,author]` | The categories and authors of all books
-|`//book[isbn]` | `$..book[?(@.isbn)]` | Filter all books with `isbn` number
-|`//book[price<10]` | `$..book[?(@.price<10)]` | Filter all books cheapier than 10
-|`//*[price>19]/..` | `$..[?(@.price>19)]` | Categories with things more expensive than 19 | Parent (caret) not present in original spec
-|`//*` | `$..*` | All elements in XML document; all members of JSON structure
+|`//book[isbn]` | `$..book[?(@.isbn)]` | Filter all books with `isbn` number
+|`//book[price<10]` | `$..book[?(@.price<10)]` | Filter all books cheapier than 10
+|`//*[price>19]/..` | `$..[?(@.price>19)]` | Categories with things more expensive than 19 | Parent (caret) not present in original spec
+|`//*` | `$..*` | All elements in XML document; all members of JSON structure
|`/store/book/[position()!=1]` | `$.store.book[?(@path !== "$[\'store\'][\'book\'][0]")]` | All books besides that at the path pointing to the first | `@path` not present in original spec
-## Script Evaluators
-
-`Hyperbee.Json` provides out-of-the-box expression evaluators for handling JsonPath filter selectors.
-
-| Name | Description |
-| ----------------------- | ----------- |
-| JsonPathCSharpEvaluator | A Roslyn based expression evaluator that supports `[(@...)]` and `[?(@...)]` expresison syntax|
-| JsonPathFuncEvaluator | A simple `Func<>` evaluator suitable for simple, custom expression handling |
-| JsonPathNullEvaluator | An evaluator that does nothing |
-
-You can create your own evaluator by deriving from `IJsonPathScriptEvaluator`.
-
-```csharp
-public class JsonPathFuncEvaluator : IJsonPathScriptEvaluator
-{
- private readonly JsonPathEvaluator _evaluator;
-
- public JsonPathFuncEvaluator( JsonPathEvaluator evaluator )
- {
- _evaluator = evaluator;
- }
-
- public object Evaluator( string script, JsonElement current, string context )
- {
- return _evaluator?.Invoke( script, current, context );
- }
-}
-```
-
-You can set a global default for the evaluator.
-
-```csharp
-JsonPath.DefaultEvaluator = new JsonPathCSharpEvaluator();
-```
-
-Or you can wire it up through dependency injection.
-
-```csharp
-public static IServiceCollection AddJsonPath( this IServiceCollection services, IConfiguration config )
-{
- services.AddTransient();
- services.AddTransient();
-
- return services;
-}
-```
-
## Code examples
A couple of trivial code examples. Review the tests for detailed examples.
@@ -180,13 +131,13 @@ const string json = @"
]";
var document = JsonDocument.Parse( json );
-var match = document.SelectPath( "$[-1:]" ).Single();
+var match = document.Select( "$[-1:]" ).Single();
Assert.IsTrue( match.Value.GetString() == "third" );
```
**Example 2** Select all elemets that have a `key` property with a value less than 42.
-This example leverages bracket expressions using the `Roslyn` jsonpath script evaluator.
+This example leverages bracket expressions using the default `Expression` jsonpath filter evaluator.
```csharp
const string json = @"
@@ -203,7 +154,7 @@ const string json = @"
]";
var document = JsonDocument.Parse( json );
-var matches = document.SelectPath( "$[?(@.key<42)]", JsonPathCSharpEvaluator.Evaluator );
+var matches = document.Select( "$[?(@.key<42)]" );
// outputs 0 -1 41 41.9999
@@ -213,13 +164,17 @@ foreach( var element in matches )
};
```
+## Helper Classes
+
+In addition to JSONPath processing, a few additional helper classes are provided to support dynamic property access,
+property diving, and element comparisons.
-## Dynamic Object Serialization
+### Dynamic Object Serialization
Basic support is provided for serializing to and from dynamic objects through the use of a custom `JsonConverter`.
The `DynamicJsonConverter` converter class is useful for simple scenareos. It is intended as a simple helper for basic use cases only.
-### DynamicJsonConverter
+#### DynamicJsonConverter
```csharp
var serializerOptions = new JsonSerializerOptions
@@ -237,16 +192,35 @@ var jsonOutput = JsonSerializer.Serialize( jobject, serializerOptions )
Assert.IsTrue( jsonInput == jsonOutput );
```
-#### Enum handling
+##### Enum handling
When deserializing, the converter will treat enumerations as strings. You can override this behavior by setting
the `TryReadValueHandler` on the converter. This handler will allow you to intercept and convert string and
numeric values during the deserialization process.
+### Equality Helpers
+
+| Method | Description
+|:-----------------------------------|:-----------
+| `JsonElement.DeepEquals` | Performs a deep equals comparison
+| `JsonElementEqualityDeepComparer` | A deep equals equality comparer
+
+### Property Diving
+
+| Method | Description
+|:-----------------------------------|:-----------
+| `JsonElement.GetPropertyFromKey` | Dives for properties using absolute bracket location keys like `$['store']['book'][2]['author']`
+
+### JsonElement Helpers
+
+| Method | Description
+|:-----------------------------------|:-----------
+| `JsonPathBuilder` | Returns the JsonPath location string for a given element
+
## Acknowlegements
This project builds on the work of:
-[Stefan Gössner - Original JSONPath specification dated 2007-02-21](http://goessner.net/articles/JsonPath/#e2)
-[Atif Aziz - .NET JSONPath](https://github.com/atifaziz/JSONPath)
-[Christoph Burgmer - Parser Consensus tests](https://cburgmer.github.io/json-path-comparison)
+* [Stefan Gössner - Original JSONPath specification dated 2007-02-21](http://goessner.net/articles/JsonPath/#e2)
+* [Atif Aziz - .NET JSONPath](https://github.com/atifaziz/JSONPath)
+* [Christoph Burgmer - Parser Consensus tests](https://cburgmer.github.io/json-path-comparison)
diff --git a/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs b/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs
new file mode 100644
index 00000000..0dcca040
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Element/ElementTypeDescriptor.cs
@@ -0,0 +1,38 @@
+using System.Text.Json;
+using Hyperbee.Json.Descriptors.Element.Functions;
+using Hyperbee.Json.Filters;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Element;
+
+public class ElementTypeDescriptor : ITypeDescriptor
+{
+ private FilterEvaluator _evaluator;
+ private ElementValueAccessor _accessor;
+ public Dictionary Functions { get; init; }
+
+ public IValueAccessor Accessor
+ {
+ get => _accessor ??= new ElementValueAccessor();
+ }
+
+ public IFilterEvaluator FilterEvaluator
+ {
+ get => _evaluator ??= new FilterEvaluator( this );
+ }
+
+ public FilterFunction GetFilterFunction( ParseExpressionContext context ) =>
+ new FilterElementFunction( context );
+
+ public ElementTypeDescriptor()
+ {
+ Functions = new Dictionary(
+ [
+ new KeyValuePair( CountElementFunction.Name, ( name, arguments, context ) => new CountElementFunction( name, arguments, context ) ),
+ new KeyValuePair( LengthElementFunction.Name, ( name, arguments, context ) => new LengthElementFunction( name, arguments, context ) ),
+ new KeyValuePair( MatchElementFunction.Name, ( name, arguments, context ) => new MatchElementFunction( name, arguments, context ) ),
+ new KeyValuePair( SearchElementFunction.Name, ( name, arguments, context ) => new SearchElementFunction( name, arguments, context ) ),
+ new KeyValuePair( ValueElementFunction.Name, ( name, arguments, context ) => new ValueElementFunction( name, arguments, context ) ),
+ ] );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs b/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs
new file mode 100644
index 00000000..5b6e8289
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Element/ElementValueAccessor.cs
@@ -0,0 +1,110 @@
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text.Json;
+
+namespace Hyperbee.Json.Descriptors.Element;
+
+internal class ElementValueAccessor : IValueAccessor
+{
+ public IEnumerable<(JsonElement, string)> EnumerateChildren( JsonElement value, bool includeValues = true )
+ {
+ switch ( value.ValueKind )
+ {
+ case JsonValueKind.Array:
+ {
+ for ( var index = value.GetArrayLength() - 1; index >= 0; index-- )
+ {
+ var child = value[index];
+
+ if ( includeValues || child.ValueKind is JsonValueKind.Array or JsonValueKind.Object )
+ yield return (child, index.ToString());
+ }
+
+ break;
+ }
+ case JsonValueKind.Object:
+ {
+ if ( includeValues )
+ {
+ foreach ( var child in value.EnumerateObject().Reverse() )
+ yield return (child.Value, child.Name);
+ }
+ else
+ {
+ foreach ( var child in value.EnumerateObject().Where( property => property.Value.ValueKind is JsonValueKind.Array or JsonValueKind.Object ).Reverse() )
+ yield return (child.Value, child.Name);
+ }
+
+ break;
+ }
+ }
+ }
+
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public JsonElement GetElementAt( in JsonElement value, int index )
+ {
+ return value[index];
+ }
+
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public bool IsObjectOrArray( in JsonElement value )
+ {
+ return value.ValueKind is JsonValueKind.Array or JsonValueKind.Object;
+ }
+
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public bool IsArray( in JsonElement value, out int length )
+ {
+ if ( value.ValueKind == JsonValueKind.Array )
+ {
+ length = value.GetArrayLength();
+ return true;
+ }
+
+ length = 0;
+ return false;
+ }
+
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public bool IsObject( in JsonElement value )
+ {
+ return value.ValueKind is JsonValueKind.Object;
+ }
+
+ public bool TryGetChildValue( in JsonElement value, string childKey, out JsonElement childValue )
+ {
+ switch ( value.ValueKind )
+ {
+ case JsonValueKind.Object:
+ if ( value.TryGetProperty( childKey, out childValue ) )
+ return true;
+ break;
+
+ case JsonValueKind.Array:
+ var index = TryParseInt( childKey ) ?? -1;
+
+ if ( index >= 0 && index < value.GetArrayLength() )
+ {
+ childValue = value[index];
+ return true;
+ }
+
+ break;
+
+ default:
+ if ( !IsPathOperator( childKey ) )
+ throw new ArgumentException( $"Invalid child type '{childKey}'. Expected child to be Object, Array or a path selector.", nameof( value ) );
+ break;
+ }
+
+ childValue = default;
+ return false;
+
+ static bool IsPathOperator( ReadOnlySpan x ) => x == "*" || x == ".." || x == "$";
+
+ static int? TryParseInt( ReadOnlySpan numberString )
+ {
+ return numberString == null ? null : int.TryParse( numberString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n ) ? n : null;
+ }
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Element/FilterElementHelper.cs b/src/Hyperbee.Json/Descriptors/Element/FilterElementHelper.cs
new file mode 100644
index 00000000..ca26050c
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Element/FilterElementHelper.cs
@@ -0,0 +1,60 @@
+using System.Reflection;
+using System.Text.Json;
+
+namespace Hyperbee.Json.Descriptors.Element;
+
+internal static class FilterElementHelper
+{
+ public static readonly MethodInfo SelectFirstElementValueMethod;
+ public static readonly MethodInfo SelectFirstMethod;
+ public static readonly MethodInfo SelectElementsMethod;
+
+ static FilterElementHelper()
+ {
+ var thisType = typeof( FilterElementHelper );
+
+ SelectFirstElementValueMethod = thisType.GetMethod( nameof( SelectFirstElementValue ), [typeof( JsonElement ), typeof( JsonElement ), typeof( string )] );
+ SelectFirstMethod = thisType.GetMethod( nameof( SelectFirst ), [typeof( JsonElement ), typeof( JsonElement ), typeof( string )] );
+ SelectElementsMethod = thisType.GetMethod( nameof( SelectElements ), [typeof( JsonElement ), typeof( JsonElement ), typeof( string )] );
+ }
+
+ private static bool IsNotEmpty( JsonElement element )
+ {
+ return element.ValueKind switch
+ {
+ JsonValueKind.Array => element.EnumerateArray().Any(),
+ JsonValueKind.Object => element.EnumerateObject().Any(),
+ _ => false
+ };
+ }
+
+ public static object SelectFirstElementValue( JsonElement current, JsonElement root, string query )
+ {
+ var element = SelectFirst( current, root, query );
+
+ return element.ValueKind switch
+ {
+ JsonValueKind.Number => element.GetSingle(),
+ JsonValueKind.String => element.GetString(),
+ JsonValueKind.Object => IsNotEmpty( element ),
+ JsonValueKind.Array => IsNotEmpty( element ),
+ JsonValueKind.True => true,
+ JsonValueKind.False => false,
+ JsonValueKind.Null => false,
+ JsonValueKind.Undefined => false,
+ _ => false
+ };
+ }
+
+ public static JsonElement SelectFirst( JsonElement current, JsonElement root, string query )
+ {
+ return SelectElements( current, root, query )
+ .FirstOrDefault();
+ }
+
+ public static IEnumerable SelectElements( JsonElement current, JsonElement root, string query )
+ {
+ return JsonPath
+ .Select( current, root, query );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs
new file mode 100644
index 00000000..461ed5c4
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Element/Functions/CountElementFunction.cs
@@ -0,0 +1,43 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Element.Functions;
+
+public class CountElementFunction( string methodName, IList arguments, ParseExpressionContext context ) :
+ FilterExtensionFunction( methodName, arguments, context )
+{
+ public const string Name = "count";
+
+ private static readonly MethodInfo CountMethod;
+
+ static CountElementFunction()
+ {
+ CountMethod = typeof( Enumerable )
+ .GetMethods( BindingFlags.Static | BindingFlags.Public )
+ .First( m =>
+ m.Name == "Count" &&
+ m.GetParameters().Length == 1 &&
+ m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) )
+ .MakeGenericMethod( typeof( JsonElement ) );
+ }
+
+ public override Expression GetExtensionExpression( string methodName, IList arguments, ParseExpressionContext context )
+ {
+ if ( arguments.Count != 1 )
+ {
+ return Expression.Throw( Expression.Constant( new Exception( $"Invalid use of {Name} function" ) ) );
+ }
+
+ var queryExp = Expression.Constant( arguments[0] );
+
+ return Expression.Convert( Expression.Call(
+ CountMethod,
+ Expression.Call( FilterElementHelper.SelectElementsMethod,
+ context.Current,
+ context.Root,
+ queryExp ) )
+ , typeof( float ) );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/FilterElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/FilterElementFunction.cs
new file mode 100644
index 00000000..c8c93c51
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Element/Functions/FilterElementFunction.cs
@@ -0,0 +1,15 @@
+using System.Linq.Expressions;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Element.Functions;
+
+public class FilterElementFunction( ParseExpressionContext context ) : FilterFunction
+{
+ protected override Expression GetExpressionImpl( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from )
+ {
+ var queryExp = Expression.Constant( item.ToString() );
+
+ // Create a call expression for the extension method
+ return Expression.Call( FilterElementHelper.SelectFirstElementValueMethod, context.Current, context.Root, queryExp );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs
new file mode 100644
index 00000000..7b320373
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Element/Functions/LengthElementFunction.cs
@@ -0,0 +1,46 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Element.Functions;
+
+public class LengthElementFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExtensionFunction( methodName, arguments, context )
+{
+ public const string Name = "length";
+
+ private static readonly MethodInfo LengthMethod;
+
+ static LengthElementFunction()
+ {
+ LengthMethod = typeof( LengthElementFunction ).GetMethod( nameof( Length ), [typeof( JsonElement )] );
+ }
+
+ public override Expression GetExtensionExpression( string methodName, IList arguments, ParseExpressionContext context )
+ {
+ if ( arguments.Count != 1 )
+ {
+ return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) );
+ }
+
+ var queryExp = Expression.Constant( arguments[0] );
+
+ return Expression.Call(
+ LengthMethod,
+ Expression.Call( FilterElementHelper.SelectFirstMethod,
+ context.Current,
+ context.Root,
+ queryExp ) );
+ }
+
+ public static float Length( JsonElement element )
+ {
+ return element.ValueKind switch
+ {
+ JsonValueKind.String => element.GetString()?.Length ?? 0,
+ JsonValueKind.Array => element.GetArrayLength(),
+ JsonValueKind.Object => element.EnumerateObject().Count(),
+ _ => 0
+ };
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs
new file mode 100644
index 00000000..0300885b
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Element/Functions/MatchElementFunction.cs
@@ -0,0 +1,46 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Element.Functions;
+
+public class MatchElementFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExtensionFunction( methodName, arguments, context )
+{
+ public const string Name = "match";
+
+ private static readonly MethodInfo MatchMethod;
+
+ static MatchElementFunction()
+ {
+ MatchMethod = typeof( MatchElementFunction ).GetMethod( nameof( Match ), [typeof( JsonElement ), typeof( string )] );
+ }
+
+ public override Expression GetExtensionExpression( string methodName, IList arguments, ParseExpressionContext context )
+ {
+ if ( arguments.Count != 2 )
+ {
+ return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) );
+ }
+
+ var queryExp = Expression.Constant( arguments[0] );
+ var regex = Expression.Constant( arguments[1] );
+
+ return Expression.Call(
+ MatchMethod,
+ Expression.Call( FilterElementHelper.SelectFirstMethod,
+ context.Current,
+ context.Root,
+ queryExp )
+ , regex );
+ }
+
+ public static bool Match( JsonElement element, string regex )
+ {
+ var regexPattern = new Regex( regex.Trim( '\"', '\'' ) );
+ var value = $"^{element.GetString()}$";
+
+ return regexPattern.IsMatch( value );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs
new file mode 100644
index 00000000..16d3b35c
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Element/Functions/SearchElementFunction.cs
@@ -0,0 +1,46 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Element.Functions;
+
+public class SearchElementFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExtensionFunction( methodName, arguments, context )
+{
+ public const string Name = "search";
+
+ private static readonly MethodInfo SearchMethod;
+
+ static SearchElementFunction()
+ {
+ SearchMethod = typeof( SearchElementFunction ).GetMethod( nameof( Search ), [typeof( JsonElement ), typeof( string )] );
+ }
+
+ public override Expression GetExtensionExpression( string methodName, IList arguments, ParseExpressionContext context )
+ {
+ if ( arguments.Count != 2 )
+ {
+ return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) );
+ }
+
+ var queryExp = Expression.Constant( arguments[0] );
+ var regex = Expression.Constant( arguments[1] );
+
+ return Expression.Call(
+ SearchMethod,
+ Expression.Call( FilterElementHelper.SelectFirstMethod,
+ context.Current,
+ context.Root,
+ queryExp )
+ , regex );
+ }
+
+ public static bool Search( JsonElement element, string regex )
+ {
+ var regexPattern = new Regex( regex.Trim( '\"', '\'' ) );
+ var value = element.GetString();
+
+ return value != null && regexPattern.IsMatch( value );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs b/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs
new file mode 100644
index 00000000..9c7078cb
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Element/Functions/ValueElementFunction.cs
@@ -0,0 +1,25 @@
+using System.Linq.Expressions;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Element.Functions;
+
+public class ValueElementFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExtensionFunction( methodName, arguments, context )
+{
+ public const string Name = "value";
+
+ public override Expression GetExtensionExpression( string methodName, IList arguments, ParseExpressionContext context )
+ {
+ if ( arguments.Count != 1 )
+ {
+ return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) );
+ }
+
+ var queryExp = Expression.Constant( arguments[0] );
+
+ return Expression.Call(
+ FilterElementHelper.SelectFirstElementValueMethod,
+ context.Current,
+ context.Root,
+ queryExp );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs b/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs
new file mode 100644
index 00000000..99002dad
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/ITypeDescriptor.cs
@@ -0,0 +1,24 @@
+using Hyperbee.Json.Filters;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors;
+
+
+public interface IJsonTypeDescriptor
+{
+ public Dictionary Functions { get; }
+
+ public FilterFunction GetFilterFunction( ParseExpressionContext context );
+}
+
+public interface ITypeDescriptor : IJsonTypeDescriptor
+{
+ public IValueAccessor Accessor { get; }
+ public IFilterEvaluator FilterEvaluator { get; }
+
+ public void Deconstruct( out IValueAccessor valueAccessor, out IFilterEvaluator filterEvaluator )
+ {
+ valueAccessor = Accessor;
+ filterEvaluator = FilterEvaluator;
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/IValueAccessor.cs b/src/Hyperbee.Json/Descriptors/IValueAccessor.cs
new file mode 100644
index 00000000..ec60521b
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/IValueAccessor.cs
@@ -0,0 +1,11 @@
+namespace Hyperbee.Json.Descriptors;
+
+public interface IValueAccessor
+{
+ IEnumerable<(TNode, string)> EnumerateChildren( TNode value, bool includeValues = true );
+ TNode GetElementAt( in TNode value, int index );
+ bool IsObjectOrArray( in TNode current );
+ bool IsArray( in TNode current, out int length );
+ bool IsObject( in TNode current );
+ bool TryGetChildValue( in TNode current, string childKey, out TNode childValue );
+}
diff --git a/src/Hyperbee.Json/Descriptors/Node/FilterNodeHelper.cs b/src/Hyperbee.Json/Descriptors/Node/FilterNodeHelper.cs
new file mode 100644
index 00000000..9528b98a
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Node/FilterNodeHelper.cs
@@ -0,0 +1,63 @@
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Hyperbee.Json.Extensions;
+
+namespace Hyperbee.Json.Descriptors.Node;
+
+internal static class FilterNodeHelper
+{
+ public static readonly MethodInfo SelectFirstElementValueMethod;
+ public static readonly MethodInfo SelectFirstMethod;
+ public static readonly MethodInfo SelectElementsMethod;
+
+ static FilterNodeHelper()
+ {
+ var thisType = typeof( FilterNodeHelper );
+
+ SelectFirstElementValueMethod = thisType.GetMethod( nameof( SelectFirstElementValue ), [typeof( JsonNode ), typeof( JsonNode ), typeof( string )] );
+ SelectFirstMethod = thisType.GetMethod( nameof( SelectFirst ), [typeof( JsonNode ), typeof( JsonNode ), typeof( string )] );
+ SelectElementsMethod = thisType.GetMethod( nameof( SelectElements ), [typeof( JsonNode ), typeof( JsonNode ), typeof( string )] );
+ }
+
+ private static bool IsNotEmpty( JsonNode node )
+ {
+ return node.GetValueKind() switch
+ {
+ JsonValueKind.Array => node.AsArray().Count != 0,
+ JsonValueKind.Object => node.AsObject().Count != 0,
+ _ => false
+ };
+ }
+
+ public static object SelectFirstElementValue( JsonNode current, JsonNode root, string query )
+ {
+ var node = SelectFirst( current, root, query );
+
+ return node?.GetValueKind() switch
+ {
+ JsonValueKind.Number => node.GetNumber(),
+ JsonValueKind.String => node.GetValue(),
+ JsonValueKind.Object => IsNotEmpty( node ),
+ JsonValueKind.Array => IsNotEmpty( node ),
+ JsonValueKind.True => true,
+ JsonValueKind.False => false,
+ JsonValueKind.Null => false,
+ JsonValueKind.Undefined => false,
+ _ => false
+ };
+ }
+
+ public static JsonNode SelectFirst( JsonNode current, JsonNode root, string query )
+ {
+ return JsonPath
+ .Select( current, root, query )
+ .FirstOrDefault();
+ }
+
+ public static IEnumerable SelectElements( JsonNode current, JsonNode root, string query )
+ {
+ return JsonPath
+ .Select( current, root, query );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs
new file mode 100644
index 00000000..6aa6f345
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Node/Functions/CountNodeFunction.cs
@@ -0,0 +1,43 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json.Nodes;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Node.Functions;
+
+public class CountNodeFunction( string methodName, IList arguments, ParseExpressionContext context ) :
+ FilterExtensionFunction( methodName, arguments, context )
+{
+ public const string Name = "count";
+
+ private static readonly MethodInfo CountMethod;
+
+ static CountNodeFunction()
+ {
+ CountMethod = typeof( Enumerable )
+ .GetMethods( BindingFlags.Static | BindingFlags.Public )
+ .First( m =>
+ m.Name == "Count" &&
+ m.GetParameters().Length == 1 &&
+ m.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == typeof( IEnumerable<> ) )
+ .MakeGenericMethod( typeof( JsonNode ) );
+ }
+
+ public override Expression GetExtensionExpression( string methodName, IList arguments, ParseExpressionContext context )
+ {
+ if ( arguments.Count != 1 )
+ {
+ return Expression.Throw( Expression.Constant( new Exception( $"Invalid use of {Name} function" ) ) );
+ }
+
+ var queryExp = Expression.Constant( arguments[0] );
+
+ return Expression.Convert( Expression.Call(
+ CountMethod,
+ Expression.Call( FilterNodeHelper.SelectElementsMethod,
+ context.Current,
+ context.Root,
+ queryExp ) )
+ , typeof( float ) );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/FilterNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/FilterNodeFunction.cs
new file mode 100644
index 00000000..9b8ed8bb
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Node/Functions/FilterNodeFunction.cs
@@ -0,0 +1,15 @@
+using System.Linq.Expressions;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Node.Functions;
+
+public class FilterNodeFunction( ParseExpressionContext context ) : FilterFunction
+{
+ protected override Expression GetExpressionImpl( ReadOnlySpan data, ReadOnlySpan item, ref int start, ref int from )
+ {
+ var queryExp = Expression.Constant( item.ToString() );
+
+ // Create a call expression for the extension method
+ return Expression.Call( FilterNodeHelper.SelectFirstElementValueMethod, context.Current, context.Root, queryExp );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs
new file mode 100644
index 00000000..9b313967
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Node/Functions/LengthNodeFunction.cs
@@ -0,0 +1,47 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Node.Functions;
+
+public class LengthNodeFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExtensionFunction( methodName, arguments, context )
+{
+ public const string Name = "length";
+
+ private static readonly MethodInfo LengthMethod;
+
+ static LengthNodeFunction()
+ {
+ LengthMethod = typeof( LengthNodeFunction ).GetMethod( nameof( Length ), [typeof( JsonNode )] );
+ }
+
+ public override Expression GetExtensionExpression( string methodName, IList arguments, ParseExpressionContext context )
+ {
+ if ( arguments.Count != 1 )
+ {
+ return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) );
+ }
+
+ var queryExp = Expression.Constant( arguments[0] );
+
+ return Expression.Call(
+ LengthMethod,
+ Expression.Call( FilterNodeHelper.SelectFirstMethod,
+ context.Current,
+ context.Root,
+ queryExp ) );
+ }
+
+ public static float Length( JsonNode node )
+ {
+ return node.GetValueKind() switch
+ {
+ JsonValueKind.String => node.GetValue()?.Length ?? 0,
+ JsonValueKind.Array => node.AsArray().Count,
+ JsonValueKind.Object => node.AsObject().Count,
+ _ => 0
+ };
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs
new file mode 100644
index 00000000..56c86ab7
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Node/Functions/MatchNodeFunction.cs
@@ -0,0 +1,46 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json.Nodes;
+using System.Text.RegularExpressions;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Node.Functions;
+
+public class MatchNodeFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExtensionFunction( methodName, arguments, context )
+{
+ public const string Name = "match";
+
+ private static readonly MethodInfo MatchMethod;
+
+ static MatchNodeFunction()
+ {
+ MatchMethod = typeof( MatchNodeFunction ).GetMethod( nameof( Match ), [typeof( JsonNode ), typeof( string )] );
+ }
+
+ public override Expression GetExtensionExpression( string methodName, IList arguments, ParseExpressionContext context )
+ {
+ if ( arguments.Count != 2 )
+ {
+ return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) );
+ }
+
+ var queryExp = Expression.Constant( arguments[0] );
+ var regex = Expression.Constant( arguments[1] );
+
+ return Expression.Call(
+ MatchMethod,
+ Expression.Call( FilterNodeHelper.SelectFirstMethod,
+ context.Current,
+ context.Root,
+ queryExp )
+ , regex );
+ }
+
+ public static bool Match( JsonNode node, string regex )
+ {
+ var regexPattern = new Regex( regex.Trim( '\"', '\'' ) );
+ var value = $"^{node.GetValue()}$";
+
+ return regexPattern.IsMatch( value );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/SearchElementFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/SearchElementFunction.cs
new file mode 100644
index 00000000..c6322712
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Node/Functions/SearchElementFunction.cs
@@ -0,0 +1,47 @@
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text.Json.Nodes;
+using System.Text.RegularExpressions;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Node.Functions;
+
+public class SearchNodeFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExtensionFunction( methodName, arguments, context )
+{
+ public const string Name = "search";
+
+ private static readonly MethodInfo SearchMethod;
+
+ static SearchNodeFunction()
+ {
+ SearchMethod = typeof( SearchNodeFunction ).GetMethod( nameof( Search ), [typeof( JsonNode ), typeof( string )] );
+ }
+
+ public override Expression GetExtensionExpression( string methodName, IList arguments, ParseExpressionContext context )
+ {
+ if ( arguments.Count != 2 )
+ {
+ return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) );
+ }
+
+ var queryExp = Expression.Constant( arguments[0] );
+ var regex = Expression.Constant( arguments[1] );
+
+ return Expression.Call(
+ SearchMethod,
+ Expression.Call( FilterNodeHelper.SelectFirstMethod,
+ context.Current,
+ context.Root,
+ queryExp )
+ , regex );
+ }
+
+ public static bool Search( JsonNode node, string regex )
+ {
+ var regexPattern = new Regex( regex.Trim( '\"', '\'' ) );
+ var value = node.GetValue();
+
+ return value != null && regexPattern.IsMatch( value );
+ }
+
+}
diff --git a/src/Hyperbee.Json/Descriptors/Node/Functions/ValueElementFunction.cs b/src/Hyperbee.Json/Descriptors/Node/Functions/ValueElementFunction.cs
new file mode 100644
index 00000000..d34d0b32
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Node/Functions/ValueElementFunction.cs
@@ -0,0 +1,25 @@
+using System.Linq.Expressions;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Node.Functions;
+
+public class ValueNodeFunction( string methodName, IList arguments, ParseExpressionContext context ) : FilterExtensionFunction( methodName, arguments, context )
+{
+ public const string Name = "value";
+
+ public override Expression GetExtensionExpression( string methodName, IList arguments, ParseExpressionContext context )
+ {
+ if ( arguments.Count != 1 )
+ {
+ return Expression.Throw( Expression.Constant( new ArgumentException( $"{Name} function has invalid parameter count." ) ) );
+ }
+
+ var queryExp = Expression.Constant( arguments[0] );
+
+ return Expression.Call(
+ FilterNodeHelper.SelectFirstElementValueMethod,
+ context.Current,
+ context.Root,
+ queryExp );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs b/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs
new file mode 100644
index 00000000..42003a59
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Node/NodeTypeDescriptor.cs
@@ -0,0 +1,38 @@
+using System.Text.Json.Nodes;
+using Hyperbee.Json.Descriptors.Node.Functions;
+using Hyperbee.Json.Filters;
+using Hyperbee.Json.Filters.Parser;
+
+namespace Hyperbee.Json.Descriptors.Node;
+
+public class NodeTypeDescriptor : ITypeDescriptor
+{
+ private FilterEvaluator _evaluator;
+ private NodeValueAccessor _accessor;
+ public Dictionary Functions { get; init; }
+
+ public IValueAccessor Accessor
+ {
+ get => _accessor ??= new NodeValueAccessor();
+ }
+
+ public IFilterEvaluator FilterEvaluator
+ {
+ get => _evaluator ??= new FilterEvaluator( this );
+ }
+
+ public FilterFunction GetFilterFunction( ParseExpressionContext context ) =>
+ new FilterNodeFunction( context );
+
+ public NodeTypeDescriptor()
+ {
+ Functions = new Dictionary(
+ [
+ new KeyValuePair( CountNodeFunction.Name, ( name, arguments, context ) => new CountNodeFunction( name, arguments, context ) ),
+ new KeyValuePair( LengthNodeFunction.Name, ( name, arguments, context ) => new LengthNodeFunction( name, arguments, context ) ),
+ new KeyValuePair( MatchNodeFunction.Name, ( name, arguments, context ) => new MatchNodeFunction( name, arguments, context ) ),
+ new KeyValuePair( SearchNodeFunction.Name, ( name, arguments, context ) => new SearchNodeFunction( name, arguments, context ) ),
+ new KeyValuePair( ValueNodeFunction.Name, ( name, arguments, context ) => new ValueNodeFunction( name, arguments, context ) ),
+ ] );
+ }
+}
diff --git a/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs b/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs
new file mode 100644
index 00000000..cb4f7e3f
--- /dev/null
+++ b/src/Hyperbee.Json/Descriptors/Node/NodeValueAccessor.cs
@@ -0,0 +1,113 @@
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text.Json.Nodes;
+
+namespace Hyperbee.Json.Descriptors.Node;
+
+internal class NodeValueAccessor : IValueAccessor
+{
+ public IEnumerable<(JsonNode, string)> EnumerateChildren( JsonNode value, bool includeValues = true )
+ {
+ switch ( value )
+ {
+ case JsonArray arrayValue:
+ for ( var index = arrayValue.Count - 1; index >= 0; index-- )
+ {
+ var child = arrayValue[index];
+
+ if ( includeValues || child is JsonObject or JsonArray )
+ yield return (child, index.ToString());
+ }
+
+ break;
+ case JsonObject objectValue:
+
+ if ( includeValues )
+ {
+ foreach ( var child in objectValue.Reverse() )
+ yield return (child.Value, child.Key);
+ }
+ else
+ {
+ foreach ( var child in objectValue.Where( property => property.Value is JsonObject or JsonArray ).Reverse() )
+ yield return (child.Value, child.Key);
+ }
+
+ break;
+ }
+ }
+
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public JsonNode GetElementAt( in JsonNode value, int index )
+ {
+ return value[index];
+ }
+
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public bool IsObjectOrArray( in JsonNode value )
+ {
+ return value is JsonObject or JsonArray;
+ }
+
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public bool IsArray( in JsonNode value, out int length )
+ {
+ if ( value is JsonArray jsonArray )
+ {
+ length = jsonArray.Count;
+ return true;
+ }
+
+ length = 0;
+ return false;
+ }
+
+ [MethodImpl( MethodImplOptions.AggressiveInlining )]
+ public bool IsObject( in JsonNode value )
+ {
+ return value is JsonObject;
+ }
+
+ public bool TryGetChildValue( in JsonNode value, string childKey, out JsonNode childValue )
+ {
+ switch ( value )
+ {
+ case JsonObject valueObject:
+ {
+ if ( valueObject.TryGetPropertyValue( childKey, out childValue ) )
+ return true;
+
+ break;
+ }
+ case JsonArray valueArray:
+ {
+ var index = TryParseInt( childKey ) ?? -1;
+
+ if ( index >= 0 && index < valueArray.Count )
+ {
+ childValue = value[index];
+ return true;
+ }
+
+ break;
+ }
+ default:
+ {
+ if ( !IsPathOperator( childKey ) )
+ throw new ArgumentException( $"Invalid child type '{childKey}'. Expected child to be Object, Array or a path selector.", nameof( value ) );
+
+ break;
+ }
+ }
+
+ childValue = default;
+ return false;
+
+ static bool IsPathOperator( ReadOnlySpan x ) => x == "*" || x == ".." || x == "$";
+
+ static int? TryParseInt( ReadOnlySpan numberString )
+ {
+ return numberString == null ? null : int.TryParse( numberString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n ) ? n : null;
+ }
+ }
+}
diff --git a/src/Hyperbee.Json/Dynamic/DynamicJsonConverter.cs b/src/Hyperbee.Json/Dynamic/DynamicJsonConverter.cs
index e371f1bf..2e0e66e1 100644
--- a/src/Hyperbee.Json/Dynamic/DynamicJsonConverter.cs
+++ b/src/Hyperbee.Json/Dynamic/DynamicJsonConverter.cs
@@ -76,7 +76,7 @@ private dynamic InternalRead( ref Utf8JsonReader reader, Type typeToConvert, Jso
case JsonTokenType.StartArray:
- IList