This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This is a .NET C# library that provides data structures and utilities for working with JSON:API (https://jsonapi.org/), a specification for building RESTful APIs. The library is named "netjac" (NET JSON:API Client) and acts as a client-side deserialization framework that maps JSON:API responses into strongly-typed C# objects.
Target Framework: .NET 8.0 Test Framework: xUnit JSON Serialization: System.Text.Json (built-in)
dotnet build Crews.Web.JsonApiClient.slndotnet test Crews.Web.JsonApiClient.slndotnet test Crews.Web.JsonApiClient.Tests/Crews.Web.JsonApiClient.Tests.csproj --settings Crews.Web.JsonApiClient.Tests/.runsettings --collect:"XPlat Code Coverage"The .runsettings file configures code coverage to output in LCOV format.
dotnet test --filter "FullyQualifiedName~Crews.Web.JsonApiClient.Tests.Converters.JsonApiLinkConverterTests"dotnet test --filter "FullyQualifiedName~Crews.Web.JsonApiClient.Tests.JsonApiDocumentTests.HasCollectionResourceReturnsTrueForArray"The library follows a composition-based architecture aligned with the JSON:API specification v1.1:
JsonApiDocument (base class)
├── Data (JsonElement?) - primary payload
├── Errors (IEnumerable<JsonApiError>?)
├── Included (IEnumerable<JsonApiResource>?)
├── Links (JsonApiLinksObject?)
├── Metadata (JsonObject?)
├── JsonApi (JsonApiInfo?)
└── Extensions (Dictionary<string, JsonElement>?)
├── JsonApiDocument<T> - strongly-typed single resource document
│ └── Data (JsonApiResource<T>?) - resource with attributes type T
└── JsonApiCollectionDocument<T> - strongly-typed collection document
└── Data (IEnumerable<JsonApiResource<T>>?) - collection of resources with attributes type T
JsonApiResource (extends JsonApiResourceIdentifier)
├── Type/Id/LocalId (identification)
├── Attributes (JsonObject?)
├── Relationships (Dictionary<string, JsonApiRelationship>?)
├── Links (JsonApiLinksObject?)
├── Metadata (JsonObject?)
└── Extensions (Dictionary<string, JsonElement>?)
├── JsonApiResource<T> - strongly-typed attributes
│ └── Attributes (T?)
└── JsonApiResource<TAttributes, TRelationships> - strongly-typed attributes and relationships
├── Attributes (TAttributes?)
└── Relationships (TRelationships?)
JsonApiRelationship
├── Links (JsonApiLinksObject?)
├── Data (JsonElement?) - ResourceIdentifier or array
├── Metadata (JsonObject?)
└── Extensions (Dictionary<string, JsonElement>?)
├── JsonApiRelationship<T> - strongly-typed single resource identifier
│ └── Data (T?) where T : JsonApiResourceIdentifier
└── JsonApiCollectionRelationship<T> - strongly-typed identifier collection
└── Data (T?) where T : IEnumerable<JsonApiResourceIdentifier>
JsonApiLink
├── Href (Uri) - required
├── Rel (string?)
├── DescribedBy (JsonApiLink?)
├── Title (string?)
├── Type (string?)
├── HrefLang (string or string[]?)
├── Metadata (JsonObject?)
└── Extensions (Dictionary<string, JsonElement>?)
-
Base Class with Generic Subclasses:
JsonApiDocument,JsonApiResource, andJsonApiRelationshipserve as flexible base classes usingJsonElement/JsonObjectfor weakly-typed scenarios, while generic subclasses provide compile-time type safety:JsonApiDocument<T>/JsonApiCollectionDocument<T>- strongly-typed document dataJsonApiResource<T>/JsonApiResource<TAttributes, TRelationships>- strongly-typed resource attributes and relationshipsJsonApiRelationship<T>/JsonApiCollectionRelationship<T>- strongly-typed relationship dataHasCollectionResource/HasErrors- check document type- Static
Deserialize()methods on all document types for easy JSON deserialization
-
Dual-Format Serialization:
JsonApiLinkConverterhandles JSON:API links, which can be either:- Simple strings:
"https://example.com" - Complex objects:
{"href": "...", "rel": "self", "title": "...", "describedBy": {...}, "meta": {...}}
- Simple strings:
-
Fluent Builder:
MediaTypeHeaderBuilderconstructs JSON:API-compliant HTTP headers with extensions and profiles -
Extension Points:
[JsonExtensionData]attributes enable JSON:API extensions without code changes -
Flexible Data Storage:
JsonObjectandJsonElementused in base classes for Attributes, Metadata, and relationship Data to avoid premature schema commitments, with generic subclasses available when schema is known -
Nullable Reference Types: All properties properly annotated with nullable reference types for compile-time null safety
Weakly-Typed Approach (flexible, schema-agnostic):
Raw JSON:API Response
↓ (JsonApiDocument.Deserialize() or JsonSerializer.Deserialize<JsonApiDocument>())
JsonApiDocument instance (Data as JsonElement)
↓ (check HasErrors, HasCollectionResource)
↓ (manually deserialize Data property)
JsonApiResource object(s)
├── Access Attributes (JsonObject for flexible schema)
├── Follow Relationships (Dictionary<string, JsonApiRelationship>)
├── Navigate via Links (hypermedia via JsonApiLink)
└── Read Metadata (JsonObject)
Strongly-Typed Approach (compile-time safety):
Raw JSON:API Response
↓ (JsonApiDocument.Deserialize<MyAttributes>() or JsonSerializer.Deserialize<JsonApiDocument<MyAttributes>>())
JsonApiDocument<MyAttributes> instance (Data as JsonApiResource<MyAttributes>)
↓ (check HasErrors)
JsonApiResource<MyAttributes> object
├── Access Attributes (MyAttributes with typed properties)
├── Follow Relationships (Dictionary<string, JsonApiRelationship>)
├── Navigate via Links (hypermedia via JsonApiLink)
└── Read Metadata (JsonObject)
- All properties use
[JsonPropertyName]for camelCase mapping (per JSON:API spec) [JsonExtensionData]captures unknown properties for forward compatibility- Leverages
JsonElementfor flexible, unstructured data - Nullable reference types enabled (
<Nullable>enable</Nullable>)
JsonApiResourceIdentifier.Type- required per JSON:API specJsonApiResourceIdentifier.IdorJsonApiResourceIdentifier.LocalId- at least one required for resource identificationJsonApiLink.Href- required for all links
- Tests organized by feature area matching source structure (
Converters/,Utility/) - xUnit test framework with Visual Studio integration
- Code coverage via coverlet.msbuild and coverlet.collector (LCOV format via
.runsettings) - Tests target .NET 10.0 (while library targets .NET 8.0 for compatibility)
- Comprehensive test coverage for
JsonApiDocumentincluding:- All property deserialization and serialization
- Helper methods (
HasCollectionResource,HasErrors) - Static
Deserialize()methods on document classes - Generic subclass deserialization for strongly-typed scenarios
- Roundtrip serialization tests
- Extension data handling
Crews.Web.JsonApiClient/ # Main library (.NET 8.0)
├── JsonApiDocument.cs # Abstract base class for all JSON:API documents
├── JsonApiResource.cs # Resource objects with attributes, relationships
├── JsonApiResourceIdentifier.cs # Type/Id identification for resources
├── JsonApiRelationship.cs # Resource relationships
├── JsonApiLink.cs # Link objects (href, metadata, etc.)
├── JsonApiLinksObject.cs # Collection of named links (self, related, etc.)
├── JsonApiError.cs # Error objects
├── JsonApiErrorSource.cs # Source pointer for errors
├── JsonApiErrorLinksObject.cs # Links specific to error objects
├── JsonApiInfo.cs # jsonapi member (version, meta)
├── Converters/
│ └── JsonApiLinkConverter.cs # Custom converter for link string/object duality
└── Utility/
├── Constants.cs # Media types, parameters, exception messages
└── MediaTypeHeaderBuilder.cs # Fluent builder for JSON:API Accept/Content-Type headers
Crews.Web.JsonApiClient.Tests/ # Test project (.NET 10.0)
├── JsonApiDocumentTests.cs # Comprehensive tests for JsonApiDocument (24 tests)
├── Converters/
│ └── JsonApiLinkConverterTests.cs # Tests for link converter (16 tests)
├── Utility/
│ └── MediaTypeHeaderBuilderTests.cs # Tests for header builder (9 tests)
├── GlobalSuppressions.cs # Code analysis suppressions
└── .runsettings # Test execution configuration (LCOV coverage)
- The project uses implicit usings and file-scoped namespaces (C# 10+ features)
- Documentation XML generation is enabled for the main library (
<GenerateDocumentationFile>true</GenerateDocumentationFile>) - All core classes use XML documentation comments for IntelliSense support
- Main branch is
master - All classes follow the
JsonApi*naming convention for clarity and consistency - Dependencies:
- Main library:
Microsoft.Net.Http.Headers(v8.0.22) for media type header handling - Test project: xUnit (v2.9.3), coverlet (v6.0.4), Microsoft.NET.Test.Sdk (v18.0.1)
- Main library:
The library has comprehensive test coverage across all major components (49 total tests):
- JsonApiDocumentTests.cs: 24 tests covering all aspects of the document model
- HasCollectionResource, HasErrors property tests
- Static Deserialize() method tests
- Property deserialization (JsonApi, Links, Included, Metadata, Errors, Extensions)
- Serialization and roundtrip tests for all document types
- JsonApiLinkConverterTests.cs: 16 tests for dual-format link serialization
- MediaTypeHeaderBuilderTests.cs: 9 tests for fluent header construction with extensions and profiles
The dev branch introduces generic subclasses that enable strongly-typed deserialization while maintaining backward compatibility with the weakly-typed base classes:
-
JsonApiDocument - Strongly-typed single resource document
Dataproperty is typed asJsonApiResource<T>?whereTis the attributes type- Includes static
Deserialize<T>()method on base class for easy JSON parsing - Example:
JsonApiDocument.Deserialize<UserAttributes>(json)returnsJsonApiDocument<UserAttributes>
-
JsonApiCollectionDocument - Strongly-typed collection document
Dataproperty is typed asIEnumerable<JsonApiResource<T>>?whereTis the attributes type- Includes static
DeserializeCollection<T>()method on base class - Example:
JsonApiDocument.DeserializeCollection<UserAttributes>(json)returnsJsonApiCollectionDocument<UserAttributes>
-
JsonApiResource - Resource with strongly-typed attributes
Attributesproperty is typed asT?instead ofJsonObject?- Example: Define
class UserResource : JsonApiResource<UserAttributes>
-
JsonApiResource<TAttributes, TRelationships> - Resource with strongly-typed attributes and relationships
Attributesproperty is typed asTAttributes?Relationshipsproperty is typed asTRelationships?instead ofDictionary<string, JsonApiRelationship>?- Example:
class UserResource : JsonApiResource<UserAttributes, UserRelationships>
-
JsonApiRelationship - Relationship with strongly-typed single resource identifier
Dataproperty is typed asT?whereT : JsonApiResourceIdentifier- Example: Used in relationship objects for to-one relationships
-
JsonApiCollectionRelationship - Relationship with strongly-typed resource identifier collection
Dataproperty is typed asT?whereT : IEnumerable<JsonApiResourceIdentifier>- Example: Used in relationship objects for to-many relationships
Removed Methods (from JsonApiDocument):
GetResource()- Previously used to deserializeDataas a single resourceGetResourceCollection()- Previously used to deserializeDataas a resource collection
Added Methods:
JsonApiDocument.Deserialize(string json, JsonSerializerOptions? options = null)- Static deserialization (weakly-typed)JsonApiDocument.Deserialize<T>(string json, JsonSerializerOptions? options = null) where T : JsonApiResource- Strongly-typed static deserializationJsonApiDocument.DeserializeCollection<T>(string json, JsonSerializerOptions? options = null)- Strongly-typed collection deserialization
Before (master branch - weakly-typed):
var doc = JsonSerializer.Deserialize<JsonApiDocument>(json);
var resource = doc.GetResource();
var userName = resource?.Attributes?["userName"]?.GetString();After (dev branch - strongly-typed option):
var doc = JsonApiDocument.Deserialize<UserAttributes>(json);
var userName = doc.Data?.Attributes?.UserName;Or continue using weakly-typed approach:
var doc = JsonApiDocument.Deserialize(json);
var resource = doc.Data?.Deserialize<JsonApiResource>();
var userName = resource?.Attributes?["userName"]?.GetString();- Compile-time type safety: Catch errors at compile time instead of runtime
- IntelliSense support: Auto-completion for properties on typed attributes and relationships
- Refactoring support: IDE can track property renames and updates
- Backward compatibility: Base classes remain unchanged, existing code continues to work
- Opt-in: Use generics only when beneficial; fall back to flexible
JsonObject/JsonElementwhen schema is unknown