Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"permissions": {
"allow": [
"Bash(dotnet test:*)",
"Bash(git checkout:*)",
"Bash(dotnet build:*)",
"Bash(ls:*)",
"Bash(find /Users/tommyc/Repositories/twcrews/jsonapi-client/Crews.Web.JsonApiClient.Tests -name \"*Tests.cs\" -exec sh -c 'echo \"\"$1: $\\(grep -c \"\"public void\\\\|public async Task\\\\|public Task\"\" \"\"$1\"\"\\)\"\" ' _ {} ;)"
],
"deny": [],
"ask": []
}
}
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.0.0] - 2026-02-09

### Added

- Generic subclasses for strongly-typed deserialization:
- `JsonApiDocument<T>` - Strongly-typed single resource document where `Data` is typed as `T?` (where `T : JsonApiResource`)
- `JsonApiCollectionDocument<T>` - Strongly-typed collection document where `Data` is typed as `T?` (where `T : IEnumerable<JsonApiResource>`)
- `JsonApiResource<T>` - Resource with strongly-typed `Attributes` property (typed as `T?` instead of `JsonObject?`)
- `JsonApiResource<TAttributes, TRelationships>` - Resource with strongly-typed `Attributes` and `Relationships` properties
- `JsonApiRelationship<T>` - Relationship with strongly-typed single resource identifier where `Data` is typed as `T?` (where `T : JsonApiResourceIdentifier`)
- `JsonApiCollectionRelationship<T>` - Relationship with strongly-typed resource identifier collection where `Data` is typed as `T?` (where `T : IEnumerable<JsonApiResourceIdentifier>`)
- Static `Deserialize(string json, JsonSerializerOptions? options = null)` methods on all document classes for convenient JSON parsing
- Extension methods for `HttpResponseMessage.Content` to deserialize JSON:API documents directly from HTTP responses:
- `ReadJsonApiDocumentAsync`
- `ReadJsonApiDocumentAsync<T>`
- `ReadJsonApiCollectionDocumentAsync<T>`

### Changed

- **Breaking change:** The `Constants` class has been moved to the `Crews.Web.JsonApiClient.Utility` namespace.

### Removed

- **Breaking change:** `GetResource()` method from `JsonApiDocument` (replaced by strongly-typed `JsonApiDocument<T>.Data` property or manual deserialization of `JsonApiDocument.Data`)
- **Breaking change:** `GetResourceCollection()` method from `JsonApiDocument` (replaced by strongly-typed `JsonApiCollectionDocument<T>.Data` property or manual deserialization of `JsonApiDocument.Data`)
- **Breaking change:** `HasSingleResource` method from `JsonApiDocument` (just use the inverse of `HasCollectionResource`)

## [2.0.0] - 2025-12-01

### Fixed
Expand All @@ -15,5 +42,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

Initial release.

[3.0.0]: https://github.com/twcrews/jsonapi-client/compare/2.0.0...3.0.0
[2.0.0]: https://github.com/twcrews/jsonapi-client/compare/1.0.0...2.0.0
[1.0.0]: https://github.com/twcrews/jsonapi-client/releases/tag/1.0.0
150 changes: 128 additions & 22 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ dotnet test --filter "FullyQualifiedName~Crews.Web.JsonApiClient.Tests.Converter

### Run a Single Test Method
```bash
dotnet test --filter "FullyQualifiedName~Crews.Web.JsonApiClient.Tests.JsonApiDocumentTests.HasSingleResourceReturnsTrueForObject"
dotnet test --filter "FullyQualifiedName~Crews.Web.JsonApiClient.Tests.JsonApiDocumentTests.HasCollectionResourceReturnsTrueForArray"
```

## Architecture Overview
Expand All @@ -45,14 +45,18 @@ The library follows a **composition-based architecture** aligned with the JSON:A
### Core Hierarchy

```
JsonApiDocument (abstract base)
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)
Expand All @@ -61,12 +65,21 @@ JsonApiResource (extends JsonApiResourceIdentifier)
├── 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
Expand All @@ -81,9 +94,12 @@ JsonApiLink

### Key Design Patterns

1. **Abstract Base Class**: `JsonApiDocument` defines the contract for all JSON:API documents and provides helper methods:
- `HasSingleResource` / `HasCollectionResource` / `HasErrors` - check document type
- `GetResource()` / `GetResourceCollection()` - safe deserialization
1. **Base Class with Generic Subclasses**: `JsonApiDocument`, `JsonApiResource`, and `JsonApiRelationship` serve as flexible base classes using `JsonElement`/`JsonObject` for weakly-typed scenarios, while generic subclasses provide compile-time type safety:
- `JsonApiDocument<T>` / `JsonApiCollectionDocument<T>` - strongly-typed document data
- `JsonApiResource<T>` / `JsonApiResource<TAttributes, TRelationships>` - strongly-typed resource attributes and relationships
- `JsonApiRelationship<T>` / `JsonApiCollectionRelationship<T>` - strongly-typed relationship data
- `HasCollectionResource` / `HasErrors` - check document type
- Static `Deserialize()` methods on all document types for easy JSON deserialization

2. **Dual-Format Serialization**: `JsonApiLinkConverter` handles JSON:API links, which can be either:
- Simple strings: `"https://example.com"`
Expand All @@ -93,20 +109,35 @@ JsonApiLink

4. **Extension Points**: `[JsonExtensionData]` attributes enable JSON:API extensions without code changes

5. **Flexible Data Storage**: `JsonObject` and `JsonElement` used for Attributes, Metadata, and relationship Data to avoid premature schema commitments
5. **Flexible Data Storage**: `JsonObject` and `JsonElement` used in base classes for Attributes, Metadata, and relationship Data to avoid premature schema commitments, with generic subclasses available when schema is known

6. **Nullable Reference Types**: All properties properly annotated with nullable reference types for compile-time null safety

### Data Flow

**Weakly-Typed Approach (flexible, schema-agnostic):**
```
Raw JSON:API Response
↓ (System.Text.Json deserializes)
JsonApiDocument instance
↓ (check HasErrors, HasSingleResource, HasCollectionResource)
↓ (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 (to other resources via JsonApiRelationship)
├── 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)
```
Expand All @@ -131,8 +162,9 @@ JsonApiResource object(s)
- Tests target .NET 10.0 (while library targets .NET 8.0 for compatibility)
- Comprehensive test coverage for `JsonApiDocument` including:
- All property deserialization and serialization
- Helper methods (`HasSingleResource`, `HasCollectionResource`, `HasErrors`)
- Resource extraction methods (`GetResource()`, `GetResourceCollection()`)
- Helper methods (`HasCollectionResource`, `HasErrors`)
- Static `Deserialize()` methods on document classes
- Generic subclass deserialization for strongly-typed scenarios
- Roundtrip serialization tests
- Extension data handling

Expand All @@ -150,18 +182,18 @@ Crews.Web.JsonApiClient/ # Main library (.NET 8.0)
├── JsonApiErrorSource.cs # Source pointer for errors
├── JsonApiErrorLinksObject.cs # Links specific to error objects
├── JsonApiInfo.cs # jsonapi member (version, meta)
├── Constants.cs # Media types, parameters, exception messages
├── 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 (31 tests)
├── JsonApiDocumentTests.cs # Comprehensive tests for JsonApiDocument (24 tests)
├── Converters/
│ └── JsonApiLinkConverterTests.cs # Tests for link converter
│ └── JsonApiLinkConverterTests.cs # Tests for link converter (16 tests)
├── Utility/
│ └── MediaTypeHeaderBuilderTests.cs # Tests for header builder
│ └── MediaTypeHeaderBuilderTests.cs # Tests for header builder (9 tests)
├── GlobalSuppressions.cs # Code analysis suppressions
└── .runsettings # Test execution configuration (LCOV coverage)
```
Expand All @@ -179,12 +211,86 @@ Crews.Web.JsonApiClient.Tests/ # Test project (.NET 10.0)

## Current Test Coverage

The library has comprehensive test coverage across all major components:
The library has comprehensive test coverage across all major components (49 total tests):

- **JsonApiDocumentTests.cs**: 31 tests covering all aspects of the document model
- HasSingleResource, HasCollectionResource, HasErrors property tests
- GetResource() and GetResourceCollection() method 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**: Tests for dual-format link serialization
- **MediaTypeHeaderBuilderTests.cs**: Tests for fluent header construction with extensions and profiles
- **JsonApiLinkConverterTests.cs**: 16 tests for dual-format link serialization
- **MediaTypeHeaderBuilderTests.cs**: 9 tests for fluent header construction with extensions and profiles

## Changes in `dev` Branch (vs. `master`)

The `dev` branch introduces **generic subclasses** that enable strongly-typed deserialization while maintaining backward compatibility with the weakly-typed base classes:

### New Generic Classes

1. **JsonApiDocument<T>** - Strongly-typed single resource document
- `Data` property is typed as `JsonApiResource<T>?` where `T` is the attributes type
- Includes static `Deserialize<T>()` method on base class for easy JSON parsing
- Example: `JsonApiDocument.Deserialize<UserAttributes>(json)` returns `JsonApiDocument<UserAttributes>`

2. **JsonApiCollectionDocument<T>** - Strongly-typed collection document
- `Data` property is typed as `IEnumerable<JsonApiResource<T>>?` where `T` is the attributes type
- Includes static `DeserializeCollection<T>()` method on base class
- Example: `JsonApiDocument.DeserializeCollection<UserAttributes>(json)` returns `JsonApiCollectionDocument<UserAttributes>`

3. **JsonApiResource<T>** - Resource with strongly-typed attributes
- `Attributes` property is typed as `T?` instead of `JsonObject?`
- Example: Define `class UserResource : JsonApiResource<UserAttributes>`

4. **JsonApiResource<TAttributes, TRelationships>** - Resource with strongly-typed attributes and relationships
- `Attributes` property is typed as `TAttributes?`
- `Relationships` property is typed as `TRelationships?` instead of `Dictionary<string, JsonApiRelationship>?`
- Example: `class UserResource : JsonApiResource<UserAttributes, UserRelationships>`

5. **JsonApiRelationship<T>** - Relationship with strongly-typed single resource identifier
- `Data` property is typed as `T?` where `T : JsonApiResourceIdentifier`
- Example: Used in relationship objects for to-one relationships

6. **JsonApiCollectionRelationship<T>** - Relationship with strongly-typed resource identifier collection
- `Data` property is typed as `T?` where `T : IEnumerable<JsonApiResourceIdentifier>`
- Example: Used in relationship objects for to-many relationships

### API Changes

**Removed Methods** (from `JsonApiDocument`):
- `GetResource()` - Previously used to deserialize `Data` as a single resource
- `GetResourceCollection()` - Previously used to deserialize `Data` as 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 deserialization
- `JsonApiDocument.DeserializeCollection<T>(string json, JsonSerializerOptions? options = null)` - Strongly-typed collection deserialization

### Migration Guide (master → dev)

**Before (master branch - weakly-typed):**
```csharp
var doc = JsonSerializer.Deserialize<JsonApiDocument>(json);
var resource = doc.GetResource();
var userName = resource?.Attributes?["userName"]?.GetString();
```

**After (dev branch - strongly-typed option):**
```csharp
var doc = JsonApiDocument.Deserialize<UserAttributes>(json);
var userName = doc.Data?.Attributes?.UserName;
```

**Or continue using weakly-typed approach:**
```csharp
var doc = JsonApiDocument.Deserialize(json);
var resource = doc.Data?.Deserialize<JsonApiResource>();
var userName = resource?.Attributes?["userName"]?.GetString();
```

### Benefits of Generic Subclasses

- **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`/`JsonElement` when schema is unknown
Loading