feat(errors): add JsonApiErrorCodes and JsonApiErrors factory methods#60
Conversation
Erlend Ellefsen (erlendellefsen)
commented
Jan 24, 2026
- Add JsonApiErrorCodes with 18 standard error codes for consistent error identification
- Add JsonApiErrors factory class with 12 methods for creating well-structured exceptions
- Factories include proper code, source, and meta fields automatically
- Add JsonApiErrorCodes with 18 standard error codes for consistent error identification - Add JsonApiErrors factory class with 12 methods for creating well-structured exceptions - Factories include proper code, source, and meta fields automatically
There was a problem hiding this comment.
Pull request overview
This PR adds a comprehensive error handling framework for JSON:API responses, introducing standardized error codes and factory methods to create consistent, well-structured exceptions.
Changes:
- Added
JsonApiErrorCodesclass with 18 standard error code constants for consistent error identification across the application - Added
JsonApiErrorsstatic factory class with 12 methods that create properly structured exception instances with appropriate HTTP status codes, error codes, source information, and metadata - Added comprehensive unit tests for all factory methods to verify correct exception creation and metadata population
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| JsonApiToolkit/Models/Errors/JsonApiErrorCodes.cs | Defines 18 constant error codes organized by category (resource, filter, include, pagination, sort, query complexity, validation, auth) |
| JsonApiToolkit/Models/Errors/JsonApiErrors.cs | Implements 12 factory methods for creating JsonApiException subclasses with proper codes, sources, and meta fields |
| JsonApiToolkit.Tests/Models/Errors/JsonApiErrorsTests.cs | Provides unit tests covering all factory methods, verifying exception types, status codes, error codes, sources, and metadata |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| [Fact] | ||
| public void AllFactories_ProduceJsonApiExceptionSubclasses() | ||
| { | ||
| var exceptions = new JsonApiException[] | ||
| { | ||
| JsonApiErrors.NotFound("books", 1), | ||
| JsonApiErrors.RelatedNotFound("books", 1, "author", 2), | ||
| JsonApiErrors.InvalidFilterValue("age", "abc", typeof(int)), | ||
| JsonApiErrors.InvalidFilterField("foo", typeof(TestClass)), | ||
| JsonApiErrors.InvalidFilterOperator("bad"), | ||
| JsonApiErrors.InvalidSortField("foo", typeof(TestClass)), | ||
| JsonApiErrors.QueryTooComplex("filters", 50, 75, "config"), | ||
| JsonApiErrors.IncludeNotAllowed("secret"), | ||
| JsonApiErrors.FilterNotAllowed("secret.field"), | ||
| JsonApiErrors.AlreadyExists("users", "email", "test@test.com"), | ||
| JsonApiErrors.ValidationFailed("email", "Invalid"), | ||
| JsonApiErrors.RequiredFieldMissing("title"), | ||
| }; | ||
|
|
||
| foreach (var ex in exceptions) | ||
| { | ||
| Assert.NotNull(ex.Code); | ||
| Assert.NotNull(ex.Message); | ||
| Assert.True(ex.StatusCode >= 400 && ex.StatusCode < 600); | ||
| } | ||
| } |
There was a problem hiding this comment.
The factory methods InvalidSortField, InvalidFilterOperator, and FilterNotAllowed lack dedicated individual test methods, unlike most other factory methods (NotFound, RelatedNotFound, InvalidFilterValue, etc.). While they are tested in the combined "AllFactories_ProduceJsonApiExceptionSubclasses" test, adding individual tests would improve test granularity, make failures easier to diagnose, and verify specific behavior like source parameters and meta fields for these methods.
| [Fact] | ||
| public void InvalidFilterField_WithoutAvailableFields_OmitsFromMeta() | ||
| { | ||
| var ex = JsonApiErrors.InvalidFilterField("foo", typeof(TestClass)); | ||
|
|
||
| Assert.NotNull(ex.Meta); | ||
| Assert.False(ex.Meta.ContainsKey("availableFields")); | ||
| } |
There was a problem hiding this comment.
The InvalidSortField factory method accepts an optional availableFields parameter, but there are no tests verifying this parameter is properly included in the meta field when provided (similar to the InvalidFilterField tests at lines 65-87). Additionally, InvalidFilterOperator's optional validOperators parameter is also untested. Add tests to verify these optional parameters are correctly handled.
| /// <summary>Creates a 404 error for a missing resource.</summary> | ||
| public static JsonApiNotFoundException NotFound(string resourceType, object id) => | ||
| new( | ||
| $"Resource '{resourceType}' with id '{id}' not found.", | ||
| JsonApiErrorCodes.ResourceNotFound, | ||
| meta: new Dictionary<string, object> { ["resourceType"] = resourceType, ["id"] = id } | ||
| ); | ||
|
|
||
| /// <summary>Creates a 404 error for a missing related resource.</summary> | ||
| public static JsonApiNotFoundException RelatedNotFound( | ||
| string resourceType, | ||
| object id, | ||
| string relationship, | ||
| object relatedId | ||
| ) => | ||
| new( | ||
| $"Related resource '{relationship}' with id '{relatedId}' not found on '{resourceType}/{id}'.", | ||
| JsonApiErrorCodes.ResourceNotFound, | ||
| meta: new Dictionary<string, object> | ||
| { | ||
| ["resourceType"] = resourceType, | ||
| ["id"] = id, | ||
| ["relationship"] = relationship, | ||
| ["relatedId"] = relatedId, | ||
| } | ||
| ); | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────── | ||
| // 400 - Bad Request (filters) | ||
| // ───────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Creates a 400 error for an invalid filter value type.</summary> | ||
| public static JsonApiBadRequestException InvalidFilterValue( | ||
| string field, | ||
| string actualValue, | ||
| Type expectedType | ||
| ) => | ||
| new( | ||
| $"Cannot convert '{actualValue}' to type {expectedType.Name} for field '{field}'.", | ||
| JsonApiErrorCodes.InvalidFilterValue, | ||
| new ErrorSource { Parameter = $"filter[{field}]" }, | ||
| new Dictionary<string, object> | ||
| { | ||
| ["field"] = field, | ||
| ["expectedType"] = expectedType.Name, | ||
| ["actualValue"] = actualValue, | ||
| } | ||
| ); | ||
|
|
||
| /// <summary>Creates a 400 error for a non-existent filter field.</summary> | ||
| public static JsonApiBadRequestException InvalidFilterField( | ||
| string field, | ||
| Type entityType, | ||
| IEnumerable<string>? availableFields = null | ||
| ) | ||
| { | ||
| var meta = new Dictionary<string, object> | ||
| { | ||
| ["field"] = field, | ||
| ["entityType"] = entityType.Name, | ||
| }; | ||
|
|
||
| if (availableFields != null) | ||
| meta["availableFields"] = availableFields.ToList(); | ||
|
|
||
| return new JsonApiBadRequestException( | ||
| $"Property '{field}' does not exist on type '{entityType.Name}'.", | ||
| JsonApiErrorCodes.InvalidFilterField, | ||
| new ErrorSource { Parameter = $"filter[{field}]" }, | ||
| meta | ||
| ); | ||
| } | ||
|
|
||
| /// <summary>Creates a 400 error for an invalid filter operator.</summary> | ||
| public static JsonApiBadRequestException InvalidFilterOperator( | ||
| string op, | ||
| IEnumerable<string>? validOperators = null | ||
| ) | ||
| { | ||
| var meta = new Dictionary<string, object> { ["operator"] = op }; | ||
|
|
||
| if (validOperators != null) | ||
| meta["validOperators"] = validOperators.ToList(); | ||
|
|
||
| return new JsonApiBadRequestException( | ||
| $"Unknown filter operator '{op}'.", | ||
| JsonApiErrorCodes.InvalidFilterOperator, | ||
| new ErrorSource { Parameter = "filter" }, | ||
| meta | ||
| ); | ||
| } | ||
|
|
||
| /// <summary>Creates a 400 error for an invalid sort field.</summary> | ||
| public static JsonApiBadRequestException InvalidSortField( | ||
| string field, | ||
| Type entityType, | ||
| IEnumerable<string>? availableFields = null | ||
| ) | ||
| { | ||
| var meta = new Dictionary<string, object> | ||
| { | ||
| ["field"] = field, | ||
| ["entityType"] = entityType.Name, | ||
| }; | ||
|
|
||
| if (availableFields != null) | ||
| meta["availableFields"] = availableFields.ToList(); | ||
|
|
||
| return new JsonApiBadRequestException( | ||
| $"Cannot sort by '{field}'. Property does not exist on type '{entityType.Name}'.", | ||
| JsonApiErrorCodes.InvalidSortField, | ||
| new ErrorSource { Parameter = "sort" }, | ||
| meta | ||
| ); | ||
| } | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────── | ||
| // 400 - Bad Request (query complexity) - for Phase 2 | ||
| // ───────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Creates a 400 error when query exceeds complexity limits.</summary> | ||
| public static JsonApiBadRequestException QueryTooComplex( | ||
| string limitName, | ||
| int limit, | ||
| int actual, | ||
| string configKey | ||
| ) => | ||
| new( | ||
| $"Query contains {actual} {limitName}, but maximum allowed is {limit}. " | ||
| + $"Reduce count or configure a higher limit via {configKey}.", | ||
| JsonApiErrorCodes.QueryTooComplex, | ||
| new ErrorSource { Parameter = "filter" }, | ||
| new Dictionary<string, object> | ||
| { | ||
| ["limitName"] = limitName, | ||
| ["limit"] = limit, | ||
| ["actual"] = actual, | ||
| ["configKey"] = configKey, | ||
| } | ||
| ); | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────── | ||
| // 403 - Forbidden | ||
| // ───────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Creates a 403 error for a disallowed include path.</summary> | ||
| public static JsonApiForbiddenException IncludeNotAllowed( | ||
| string include, | ||
| IEnumerable<string>? allowedIncludes = null | ||
| ) | ||
| { | ||
| var meta = new Dictionary<string, object> { ["requestedInclude"] = include }; | ||
|
|
||
| if (allowedIncludes != null) | ||
| meta["allowedIncludes"] = allowedIncludes.ToList(); | ||
|
|
||
| return new JsonApiForbiddenException( | ||
| $"Include path '{include}' is not allowed.", | ||
| JsonApiErrorCodes.IncludeNotAllowed, | ||
| new ErrorSource { Parameter = "include" }, | ||
| meta | ||
| ); | ||
| } | ||
|
|
||
| /// <summary>Creates a 403 error for filtering on a disallowed relationship.</summary> | ||
| public static JsonApiForbiddenException FilterNotAllowed(string relationshipPath) => | ||
| new( | ||
| $"Filtering on relationship '{relationshipPath}' is not allowed.", | ||
| JsonApiErrorCodes.FilterNotAllowed, | ||
| new ErrorSource { Parameter = $"filter[{relationshipPath}]" }, | ||
| new Dictionary<string, object> { ["relationshipPath"] = relationshipPath } | ||
| ); | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────── | ||
| // 409 - Conflict | ||
| // ───────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Creates a 409 error for duplicate resource.</summary> | ||
| public static JsonApiConflictException AlreadyExists( | ||
| string resourceType, | ||
| string field, | ||
| object value | ||
| ) => | ||
| new( | ||
| $"A '{resourceType}' with {field} '{value}' already exists.", | ||
| JsonApiErrorCodes.ResourceAlreadyExists, | ||
| new ErrorSource { Pointer = $"/data/attributes/{field}" }, | ||
| new Dictionary<string, object> | ||
| { | ||
| ["resourceType"] = resourceType, | ||
| ["field"] = field, | ||
| ["value"] = value, | ||
| } | ||
| ); | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────── | ||
| // Validation helpers | ||
| // ───────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Creates a 400 error for a validation failure.</summary> | ||
| public static JsonApiBadRequestException ValidationFailed(string field, string message) => | ||
| new( | ||
| message, | ||
| JsonApiErrorCodes.ValidationFailed, | ||
| new ErrorSource { Pointer = $"/data/attributes/{field}" }, | ||
| new Dictionary<string, object> { ["field"] = field } | ||
| ); | ||
|
|
||
| /// <summary>Creates a 400 error for a missing required field.</summary> | ||
| public static JsonApiBadRequestException RequiredFieldMissing(string field) => | ||
| new( | ||
| $"Required field '{field}' is missing.", | ||
| JsonApiErrorCodes.RequiredFieldMissing, | ||
| new ErrorSource { Pointer = $"/data/attributes/{field}" }, | ||
| new Dictionary<string, object> { ["field"] = field } | ||
| ); |
There was a problem hiding this comment.
The factory methods lack XML documentation for parameters. While the method names and parameters are relatively self-explanatory, the codebase consistently uses XML <param> tags elsewhere (see JsonApiController.cs, EntityMapper.cs, JsonApiMapper.cs). Adding parameter documentation would improve API discoverability and IDE IntelliSense support. For example, document what types of values are expected for 'resourceType', 'field', 'id', etc.
| public static JsonApiNotFoundException NotFound(string resourceType, object id) => | ||
| new( | ||
| $"Resource '{resourceType}' with id '{id}' not found.", | ||
| JsonApiErrorCodes.ResourceNotFound, | ||
| meta: new Dictionary<string, object> { ["resourceType"] = resourceType, ["id"] = id } | ||
| ); | ||
|
|
||
| /// <summary>Creates a 404 error for a missing related resource.</summary> | ||
| public static JsonApiNotFoundException RelatedNotFound( | ||
| string resourceType, | ||
| object id, | ||
| string relationship, | ||
| object relatedId | ||
| ) => | ||
| new( | ||
| $"Related resource '{relationship}' with id '{relatedId}' not found on '{resourceType}/{id}'.", | ||
| JsonApiErrorCodes.ResourceNotFound, | ||
| meta: new Dictionary<string, object> | ||
| { | ||
| ["resourceType"] = resourceType, | ||
| ["id"] = id, | ||
| ["relationship"] = relationship, | ||
| ["relatedId"] = relatedId, | ||
| } | ||
| ); |
There was a problem hiding this comment.
The 'id' and 'relatedId' parameters are typed as 'object', which allows any type to be passed. However, the string interpolation will call ToString() on these objects. If complex objects are passed, this could produce unhelpful messages like "MyNamespace.MyClass" instead of meaningful IDs. Consider either: (1) constraining the parameter type (e.g., to IConvertible or IFormattable), (2) adding validation to ensure the object has a meaningful string representation, or (3) documenting the expected parameter types (typically string, int, Guid).
| public const string IncludeDepthExceeded = "INCLUDE_DEPTH_EXCEEDED"; | ||
|
|
||
| // Pagination errors | ||
| public const string InvalidPageNumber = "INVALID_PAGE_NUMBER"; | ||
| public const string InvalidPageSize = "INVALID_PAGE_SIZE"; | ||
| public const string PageSizeExceeded = "PAGE_SIZE_EXCEEDED"; | ||
|
|
||
| // Sort errors | ||
| public const string InvalidSortField = "INVALID_SORT_FIELD"; | ||
|
|
||
| // Query complexity | ||
| public const string QueryTooComplex = "QUERY_TOO_COMPLEX"; | ||
| public const string TooManyFilters = "TOO_MANY_FILTERS"; | ||
|
|
||
| // Validation | ||
| public const string ValidationFailed = "VALIDATION_FAILED"; | ||
| public const string RequiredFieldMissing = "REQUIRED_FIELD_MISSING"; | ||
|
|
||
| // Auth | ||
| public const string AuthenticationRequired = "AUTHENTICATION_REQUIRED"; | ||
| public const string InsufficientPermissions = "INSUFFICIENT_PERMISSIONS"; |
There was a problem hiding this comment.
The error code constants "IncludeDepthExceeded", "InvalidPageNumber", "InvalidPageSize", "PageSizeExceeded", "TooManyFilters", "AuthenticationRequired", and "InsufficientPermissions" are defined but have no corresponding factory methods in JsonApiErrors.cs. While this may be intentional for future development, consider either: (1) removing unused codes until they're needed, (2) adding factory methods for them, or (3) adding a comment explaining they're reserved for future use. This would improve code clarity and prevent confusion about which error codes are currently supported.
| public static JsonApiNotFoundException NotFound(string resourceType, object id) => | ||
| new( | ||
| $"Resource '{resourceType}' with id '{id}' not found.", | ||
| JsonApiErrorCodes.ResourceNotFound, | ||
| meta: new Dictionary<string, object> { ["resourceType"] = resourceType, ["id"] = id } | ||
| ); | ||
|
|
||
| /// <summary>Creates a 404 error for a missing related resource.</summary> | ||
| public static JsonApiNotFoundException RelatedNotFound( | ||
| string resourceType, | ||
| object id, | ||
| string relationship, | ||
| object relatedId | ||
| ) => | ||
| new( | ||
| $"Related resource '{relationship}' with id '{relatedId}' not found on '{resourceType}/{id}'.", | ||
| JsonApiErrorCodes.ResourceNotFound, | ||
| meta: new Dictionary<string, object> | ||
| { | ||
| ["resourceType"] = resourceType, | ||
| ["id"] = id, | ||
| ["relationship"] = relationship, | ||
| ["relatedId"] = relatedId, | ||
| } | ||
| ); | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────── | ||
| // 400 - Bad Request (filters) | ||
| // ───────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Creates a 400 error for an invalid filter value type.</summary> | ||
| public static JsonApiBadRequestException InvalidFilterValue( | ||
| string field, | ||
| string actualValue, | ||
| Type expectedType | ||
| ) => | ||
| new( | ||
| $"Cannot convert '{actualValue}' to type {expectedType.Name} for field '{field}'.", | ||
| JsonApiErrorCodes.InvalidFilterValue, | ||
| new ErrorSource { Parameter = $"filter[{field}]" }, | ||
| new Dictionary<string, object> | ||
| { | ||
| ["field"] = field, | ||
| ["expectedType"] = expectedType.Name, | ||
| ["actualValue"] = actualValue, | ||
| } | ||
| ); | ||
|
|
||
| /// <summary>Creates a 400 error for a non-existent filter field.</summary> | ||
| public static JsonApiBadRequestException InvalidFilterField( | ||
| string field, | ||
| Type entityType, | ||
| IEnumerable<string>? availableFields = null | ||
| ) | ||
| { | ||
| var meta = new Dictionary<string, object> | ||
| { | ||
| ["field"] = field, | ||
| ["entityType"] = entityType.Name, | ||
| }; | ||
|
|
||
| if (availableFields != null) | ||
| meta["availableFields"] = availableFields.ToList(); | ||
|
|
||
| return new JsonApiBadRequestException( | ||
| $"Property '{field}' does not exist on type '{entityType.Name}'.", | ||
| JsonApiErrorCodes.InvalidFilterField, | ||
| new ErrorSource { Parameter = $"filter[{field}]" }, | ||
| meta | ||
| ); | ||
| } | ||
|
|
||
| /// <summary>Creates a 400 error for an invalid filter operator.</summary> | ||
| public static JsonApiBadRequestException InvalidFilterOperator( | ||
| string op, | ||
| IEnumerable<string>? validOperators = null | ||
| ) | ||
| { | ||
| var meta = new Dictionary<string, object> { ["operator"] = op }; | ||
|
|
||
| if (validOperators != null) | ||
| meta["validOperators"] = validOperators.ToList(); | ||
|
|
||
| return new JsonApiBadRequestException( | ||
| $"Unknown filter operator '{op}'.", | ||
| JsonApiErrorCodes.InvalidFilterOperator, | ||
| new ErrorSource { Parameter = "filter" }, | ||
| meta | ||
| ); | ||
| } | ||
|
|
||
| /// <summary>Creates a 400 error for an invalid sort field.</summary> | ||
| public static JsonApiBadRequestException InvalidSortField( | ||
| string field, | ||
| Type entityType, | ||
| IEnumerable<string>? availableFields = null | ||
| ) | ||
| { | ||
| var meta = new Dictionary<string, object> | ||
| { | ||
| ["field"] = field, | ||
| ["entityType"] = entityType.Name, | ||
| }; | ||
|
|
||
| if (availableFields != null) | ||
| meta["availableFields"] = availableFields.ToList(); | ||
|
|
||
| return new JsonApiBadRequestException( | ||
| $"Cannot sort by '{field}'. Property does not exist on type '{entityType.Name}'.", | ||
| JsonApiErrorCodes.InvalidSortField, | ||
| new ErrorSource { Parameter = "sort" }, | ||
| meta | ||
| ); | ||
| } | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────── | ||
| // 400 - Bad Request (query complexity) - for Phase 2 | ||
| // ───────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Creates a 400 error when query exceeds complexity limits.</summary> | ||
| public static JsonApiBadRequestException QueryTooComplex( | ||
| string limitName, | ||
| int limit, | ||
| int actual, | ||
| string configKey | ||
| ) => | ||
| new( | ||
| $"Query contains {actual} {limitName}, but maximum allowed is {limit}. " | ||
| + $"Reduce count or configure a higher limit via {configKey}.", | ||
| JsonApiErrorCodes.QueryTooComplex, | ||
| new ErrorSource { Parameter = "filter" }, | ||
| new Dictionary<string, object> | ||
| { | ||
| ["limitName"] = limitName, | ||
| ["limit"] = limit, | ||
| ["actual"] = actual, | ||
| ["configKey"] = configKey, | ||
| } | ||
| ); | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────── | ||
| // 403 - Forbidden | ||
| // ───────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Creates a 403 error for a disallowed include path.</summary> | ||
| public static JsonApiForbiddenException IncludeNotAllowed( | ||
| string include, | ||
| IEnumerable<string>? allowedIncludes = null | ||
| ) | ||
| { | ||
| var meta = new Dictionary<string, object> { ["requestedInclude"] = include }; | ||
|
|
||
| if (allowedIncludes != null) | ||
| meta["allowedIncludes"] = allowedIncludes.ToList(); | ||
|
|
||
| return new JsonApiForbiddenException( | ||
| $"Include path '{include}' is not allowed.", | ||
| JsonApiErrorCodes.IncludeNotAllowed, | ||
| new ErrorSource { Parameter = "include" }, | ||
| meta | ||
| ); | ||
| } | ||
|
|
||
| /// <summary>Creates a 403 error for filtering on a disallowed relationship.</summary> | ||
| public static JsonApiForbiddenException FilterNotAllowed(string relationshipPath) => | ||
| new( | ||
| $"Filtering on relationship '{relationshipPath}' is not allowed.", | ||
| JsonApiErrorCodes.FilterNotAllowed, | ||
| new ErrorSource { Parameter = $"filter[{relationshipPath}]" }, | ||
| new Dictionary<string, object> { ["relationshipPath"] = relationshipPath } | ||
| ); | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────── | ||
| // 409 - Conflict | ||
| // ───────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Creates a 409 error for duplicate resource.</summary> | ||
| public static JsonApiConflictException AlreadyExists( | ||
| string resourceType, | ||
| string field, | ||
| object value | ||
| ) => | ||
| new( | ||
| $"A '{resourceType}' with {field} '{value}' already exists.", | ||
| JsonApiErrorCodes.ResourceAlreadyExists, | ||
| new ErrorSource { Pointer = $"/data/attributes/{field}" }, | ||
| new Dictionary<string, object> | ||
| { | ||
| ["resourceType"] = resourceType, | ||
| ["field"] = field, | ||
| ["value"] = value, | ||
| } | ||
| ); | ||
|
|
||
| // ───────────────────────────────────────────────────────────────────────── | ||
| // Validation helpers | ||
| // ───────────────────────────────────────────────────────────────────────── | ||
|
|
||
| /// <summary>Creates a 400 error for a validation failure.</summary> | ||
| public static JsonApiBadRequestException ValidationFailed(string field, string message) => | ||
| new( | ||
| message, | ||
| JsonApiErrorCodes.ValidationFailed, | ||
| new ErrorSource { Pointer = $"/data/attributes/{field}" }, | ||
| new Dictionary<string, object> { ["field"] = field } | ||
| ); | ||
|
|
||
| /// <summary>Creates a 400 error for a missing required field.</summary> | ||
| public static JsonApiBadRequestException RequiredFieldMissing(string field) => | ||
| new( | ||
| $"Required field '{field}' is missing.", | ||
| JsonApiErrorCodes.RequiredFieldMissing, | ||
| new ErrorSource { Pointer = $"/data/attributes/{field}" }, | ||
| new Dictionary<string, object> { ["field"] = field } | ||
| ); |
There was a problem hiding this comment.
The factory methods don't validate input parameters for null or empty values. For example, if an empty string or null is passed for 'resourceType', 'field', 'relationship', etc., the error messages will be malformed or unclear. Consider adding parameter validation (e.g., ArgumentNullException or ArgumentException) for critical string parameters to ensure error messages are always well-formed and meaningful.
🤖 I have created a release *beep* *boop* --- ## [1.3.0](Intility.JsonApiToolkit-v1.2.5...Intility.JsonApiToolkit-v1.3.0) (2026-01-24) ### Features * ✨ `AllowedIncludesAttribute` to whitelist allowed include paths ([6a26c29](6a26c29)) * ✨ `JsonApiOkAsync` ([d22466d](d22466d)) * ✨ `JsonApiOkAsync` ([bc26940](bc26940)) * ✨ add filtering support for included resources ([4e81c99](4e81c99)) * ✨ add support for filtering in primary resource with included r… ([0b0bb87](0b0bb87)) * ✨ add support for filtering in primary resource with included relationships ([5c90d41](5c90d41)) * ✨ add too many reqyests exeption ([94a810d](94a810d)) * ✨ Allow collections and json columns to be mapped ([6a096bc](6a096bc)) * ✨ Code cleanup and standardization of error handling ([fec75f5](fec75f5)) * ✨ Enhance QueryHelpers with enum support and additional types ([4949c26](4949c26)) * ✨ general-purpose exception class ([0cdf9a5](0cdf9a5)) * ✨ general-purpose exception class ([e222bb4](e222bb4)) * ✨ Overall project cleanup ([c8c10f4](c8c10f4)) * ✨ Remove IncludeAsAttribute and related logic ([a1593be](a1593be)) * ✨ Support complex JsonCols ([b744b8e](b744b8e)) * 📚 add comprehensive debugging guide and enhance logging for better troubleshooting ([0ecc0cf](0ecc0cf)) * 🚀 add ApplyFiltersOnly method for pre-aggregation filtering and add documentation on statistics and aggregations ([b9c6546](b9c6546)) * 🚀 enhance query processing with AsSingleQuery for pagination and add detailed logging for inclusion processing ([7824da9](7824da9)) * **errors:** add JsonApiErrorCodes and JsonApiErrors factory methods ([#60](#60)) ([8531ad3](8531ad3)) * **errors:** complete refactor Phase 1 with exception filter tests a… ([#61](#61)) ([e5b50be](e5b50be)) ### Bug Fixes * 🐛 single included resources are no longer ignored ([1e2e4b6](1e2e4b6)) * 🚑️ `[JsonIgnore]` not being respected ([af12b0b](af12b0b)) * 🚑️ adds support for filtering on included collection fields ([ee2eb19](ee2eb19)) * 🚑️ adds support for filtering on included collection fields ([1194fd6](1194fd6)) * 🚑️ adjust query processing order for filtered and regular includes to enhance EF Core compatibility ([8d21509](8d21509)) * 🚑️ bracket nested filtering without the nessesary includes breaking main filtering ([e1e5785](e1e5785)) * 🚑️ correct version number in project file to match release version ([a0a51dd](a0a51dd)) * 🚑️ error responses for forbidden includes did not include meta information ([9610878](9610878)) * 🚑️ filtering on includes not working on 2-level ([c658327](c658327)) * 🚑️ Fixed the filtering issue for included resources. ([86cab81](86cab81)) * 🚑️ improve error messages for forbidden includes to clarify not found status ([07ea15e](07ea15e)) * 🚑️ Initial working fix. Needs further testing and validation. ([0fa5628](0fa5628)) * 🚑️ JsonApiOk and JsonApiCreated methods not adding includes ([903eda3](903eda3)) * 🚑️ refactor querying files and fix single resource relationship issues ([962d4d4](962d4d4)) * 🚑️ reorder query processing to apply sorting before includes for better EF Core compatibility ([20bf0d9](20bf0d9)) * 🚑️ three level nested values and collection include filters ([7f9a336](7f9a336)) * 🚑️ three level nested values and collection include filters ([044aaf0](044aaf0)) * 🚑️ use single query mode to prevent EF Core split query correlation issues with filtered includes ([ff48615](ff48615)) * add defensive reflection checks with ReflectionMethodCache ([#57](#57)) ([75eb978](75eb978)) * **mapping:** remove dead AddIncludedResourcesRecursive method ([#55](#55)) ([bbc8c17](bbc8c17)) * **pagination:** guard against division by zero when Size is 0 ([#59](#59)) ([0863dee](0863dee)) * **parsing:** guard unsafe string parsing in filter parsers ([#58](#58)) ([9fb463d](9fb463d)) * **security:** prevent log forging and add workflow permissions ([#51](#51)) ([5fbbaba](5fbbaba)) * **security:** prevent log forging and update tooling ([#52](#52)) ([52d73ce](52d73ce)) * support JsonPropertyName attribute and fix many-to-many collecti… ([634abff](634abff)) * support JsonPropertyName attribute and fix many-to-many collection filtering ([6f1d961](6f1d961)) ### Refactoring * 🔨 follow ts-package renaming ([4cd1e7e](4cd1e7e)) * 🔨 optimize logging and add XML documentation ([8c14bc0](8c14bc0)) * 🔨 remove Microsoft.Identity.Abstractions package reference ([55933b7](55933b7)) * 🔨 remove the OR max count ([65107d5](65107d5)) * 🔨 remove the OR max count ([5a3aa87](5a3aa87)) * 🔨 Update JsonApiOk function and docs to align with what it actually does ([bfe7635](bfe7635)) ### Documentation * 📝 update stats docs ([549743c](549743c)) * 📜 add too many request exeption to docs ([872ae2a](872ae2a)) * 📜 Clarify that filtering is only on main entity ([5ee3568](5ee3568)) * 📜 Update Claude.md ([88502bb](88502bb)) * 📜 update error message for forbidden includes to clarify not found status ([95ab6ce](95ab6ce)) ### Dependencies * **actions:** bump actions/checkout from 4 to 6 ([#47](#47)) ([a16ab53](a16ab53)) * **actions:** bump actions/setup-dotnet from 4 to 5 ([#45](#45)) ([db8c0d1](db8c0d1)) * **actions:** bump actions/upload-pages-artifact from 3 to 4 ([#44](#44)) ([c5e35fb](c5e35fb)) * **actions:** bump github/codeql-action from 3 to 4 ([#46](#46)) ([4bad70c](4bad70c)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: intility-release-bot[bot] <175299729+intility-release-bot[bot]@users.noreply.github.com>