Skip to content

Commit 47a1589

Browse files
authored
Merge pull request #1595 from json-api-dotnet/openapi-type-hints
OpenAPI: Add type hints on 'id' parameters and properties
2 parents 92e5590 + a18666c commit 47a1589

File tree

65 files changed

+1896
-1013
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1896
-1013
lines changed

src/Examples/JsonApiDotNetCoreExample/GeneratedSwagger/JsonApiDotNetCoreExample.json

+186-68
Large diffs are not rendered by default.

src/JsonApiDotNetCore.OpenApi/SchemaGenerators/Components/DataSchemaGenerator.cs

+18-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ internal sealed class DataSchemaGenerator
2525
private readonly SchemaGenerator _defaultSchemaGenerator;
2626
private readonly GenerationCacheSchemaGenerator _generationCacheSchemaGenerator;
2727
private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator;
28+
private readonly ResourceIdSchemaGenerator _resourceIdSchemaGenerator;
2829
private readonly ResourceIdentifierSchemaGenerator _resourceIdentifierSchemaGenerator;
2930
private readonly LinksVisibilitySchemaGenerator _linksVisibilitySchemaGenerator;
3031
private readonly IResourceGraph _resourceGraph;
@@ -34,14 +35,15 @@ internal sealed class DataSchemaGenerator
3435
private readonly ResourceDocumentationReader _resourceDocumentationReader;
3536

3637
public DataSchemaGenerator(SchemaGenerator defaultSchemaGenerator, GenerationCacheSchemaGenerator generationCacheSchemaGenerator,
37-
ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, ResourceIdentifierSchemaGenerator resourceIdentifierSchemaGenerator,
38-
LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator, IResourceGraph resourceGraph, IJsonApiOptions options,
39-
ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider, RelationshipTypeFactory relationshipTypeFactory,
40-
ResourceDocumentationReader resourceDocumentationReader)
38+
ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, ResourceIdSchemaGenerator resourceIdSchemaGenerator,
39+
ResourceIdentifierSchemaGenerator resourceIdentifierSchemaGenerator, LinksVisibilitySchemaGenerator linksVisibilitySchemaGenerator,
40+
IResourceGraph resourceGraph, IJsonApiOptions options, ResourceFieldValidationMetadataProvider resourceFieldValidationMetadataProvider,
41+
RelationshipTypeFactory relationshipTypeFactory, ResourceDocumentationReader resourceDocumentationReader)
4142
{
4243
ArgumentGuard.NotNull(defaultSchemaGenerator);
4344
ArgumentGuard.NotNull(generationCacheSchemaGenerator);
4445
ArgumentGuard.NotNull(resourceTypeSchemaGenerator);
46+
ArgumentGuard.NotNull(resourceIdSchemaGenerator);
4547
ArgumentGuard.NotNull(resourceIdentifierSchemaGenerator);
4648
ArgumentGuard.NotNull(linksVisibilitySchemaGenerator);
4749
ArgumentGuard.NotNull(resourceGraph);
@@ -53,6 +55,7 @@ public DataSchemaGenerator(SchemaGenerator defaultSchemaGenerator, GenerationCac
5355
_defaultSchemaGenerator = defaultSchemaGenerator;
5456
_generationCacheSchemaGenerator = generationCacheSchemaGenerator;
5557
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;
58+
_resourceIdSchemaGenerator = resourceIdSchemaGenerator;
5659
_resourceIdentifierSchemaGenerator = resourceIdentifierSchemaGenerator;
5760
_linksVisibilitySchemaGenerator = linksVisibilitySchemaGenerator;
5861
_resourceGraph = resourceGraph;
@@ -88,6 +91,8 @@ public OpenApiSchema GenerateSchema(Type resourceDataConstructedType, SchemaRepo
8891
SetResourceType(fullSchemaForResourceData, resourceTypeInfo.ResourceType, schemaRepository);
8992
}
9093

94+
SetResourceId(fullSchemaForDerivedType, resourceTypeInfo.ResourceType, schemaRepository);
95+
9196
fullSchemaForResourceData.Description = _resourceDocumentationReader.GetDocumentationForType(resourceTypeInfo.ResourceType);
9297

9398
var fieldSchemaBuilder = new ResourceFieldSchemaBuilder(_defaultSchemaGenerator, _resourceIdentifierSchemaGenerator, _linksVisibilitySchemaGenerator,
@@ -162,6 +167,15 @@ private void SetResourceType(OpenApiSchema fullSchemaForResourceData, ResourceTy
162167
fullSchemaForResourceData.Properties[JsonApiPropertyName.Type] = referenceSchema.WrapInExtendedSchema();
163168
}
164169

170+
private void SetResourceId(OpenApiSchema fullSchemaForResourceData, ResourceType resourceType, SchemaRepository schemaRepository)
171+
{
172+
if (fullSchemaForResourceData.Properties.ContainsKey(JsonApiPropertyName.Id))
173+
{
174+
OpenApiSchema idSchema = _resourceIdSchemaGenerator.GenerateSchema(resourceType, schemaRepository);
175+
fullSchemaForResourceData.Properties[JsonApiPropertyName.Id] = idSchema;
176+
}
177+
}
178+
165179
private void SetResourceAttributes(OpenApiSchema fullSchemaForResourceData, bool forRequestSchema, ResourceFieldSchemaBuilder builder,
166180
SchemaRepository schemaRepository)
167181
{

src/JsonApiDotNetCore.OpenApi/SchemaGenerators/Components/RelationshipIdentifierSchemaGenerator.cs

+27-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using JsonApiDotNetCore.Configuration;
12
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
23
using JsonApiDotNetCore.Resources.Annotations;
34
using Microsoft.OpenApi.Models;
@@ -19,19 +20,23 @@ internal sealed class RelationshipIdentifierSchemaGenerator
1920

2021
private readonly SchemaGenerator _defaultSchemaGenerator;
2122
private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator;
23+
private readonly ResourceIdSchemaGenerator _resourceIdSchemaGenerator;
2224
private readonly RelationshipNameSchemaGenerator _relationshipNameSchemaGenerator;
2325
private readonly JsonApiSchemaIdSelector _jsonApiSchemaIdSelector;
2426

2527
public RelationshipIdentifierSchemaGenerator(SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator,
26-
RelationshipNameSchemaGenerator relationshipNameSchemaGenerator, JsonApiSchemaIdSelector jsonApiSchemaIdSelector)
28+
ResourceIdSchemaGenerator resourceIdSchemaGenerator, RelationshipNameSchemaGenerator relationshipNameSchemaGenerator,
29+
JsonApiSchemaIdSelector jsonApiSchemaIdSelector)
2730
{
2831
ArgumentGuard.NotNull(defaultSchemaGenerator);
2932
ArgumentGuard.NotNull(resourceTypeSchemaGenerator);
33+
ArgumentGuard.NotNull(resourceIdSchemaGenerator);
3034
ArgumentGuard.NotNull(relationshipNameSchemaGenerator);
3135
ArgumentGuard.NotNull(jsonApiSchemaIdSelector);
3236

3337
_defaultSchemaGenerator = defaultSchemaGenerator;
3438
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;
39+
_resourceIdSchemaGenerator = resourceIdSchemaGenerator;
3540
_relationshipNameSchemaGenerator = relationshipNameSchemaGenerator;
3641
_jsonApiSchemaIdSelector = jsonApiSchemaIdSelector;
3742
}
@@ -65,11 +70,9 @@ public OpenApiSchema GenerateSchema(RelationshipAttribute relationship, SchemaRe
6570
OpenApiSchema referenceSchemaForIdentifier = _defaultSchemaGenerator.GenerateSchema(relationshipIdentifierConstructedType, schemaRepository);
6671
OpenApiSchema fullSchemaForIdentifier = schemaRepository.Schemas[referenceSchemaForIdentifier.Reference.Id];
6772

68-
OpenApiSchema referenceSchemaForResourceType = _resourceTypeSchemaGenerator.GenerateSchema(relationship.LeftType, schemaRepository);
69-
fullSchemaForIdentifier.Properties[JsonApiPropertyName.Type] = referenceSchemaForResourceType.WrapInExtendedSchema();
70-
71-
OpenApiSchema referenceSchemaForRelationshipName = _relationshipNameSchemaGenerator.GenerateSchema(relationship, schemaRepository);
72-
fullSchemaForIdentifier.Properties[JsonApiPropertyName.Relationship] = referenceSchemaForRelationshipName.WrapInExtendedSchema();
73+
SetResourceType(fullSchemaForIdentifier, relationship.LeftType, schemaRepository);
74+
SetResourceId(fullSchemaForIdentifier, relationship.LeftType, schemaRepository);
75+
SetRelationship(fullSchemaForIdentifier, relationship, schemaRepository);
7376

7477
#if NET6_0
7578
fullSchemaForIdentifier.ReorderProperties(RelationshipIdentifierPropertyNamesInOrder);
@@ -80,4 +83,22 @@ public OpenApiSchema GenerateSchema(RelationshipAttribute relationship, SchemaRe
8083

8184
return referenceSchemaForIdentifier;
8285
}
86+
87+
private void SetResourceType(OpenApiSchema fullSchemaForIdentifier, ResourceType resourceType, SchemaRepository schemaRepository)
88+
{
89+
OpenApiSchema referenceSchema = _resourceTypeSchemaGenerator.GenerateSchema(resourceType, schemaRepository);
90+
fullSchemaForIdentifier.Properties[JsonApiPropertyName.Type] = referenceSchema.WrapInExtendedSchema();
91+
}
92+
93+
private void SetResourceId(OpenApiSchema fullSchemaForResourceData, ResourceType resourceType, SchemaRepository schemaRepository)
94+
{
95+
OpenApiSchema idSchema = _resourceIdSchemaGenerator.GenerateSchema(resourceType, schemaRepository);
96+
fullSchemaForResourceData.Properties[JsonApiPropertyName.Id] = idSchema;
97+
}
98+
99+
private void SetRelationship(OpenApiSchema fullSchemaForIdentifier, RelationshipAttribute relationship, SchemaRepository schemaRepository)
100+
{
101+
OpenApiSchema referenceSchema = _relationshipNameSchemaGenerator.GenerateSchema(relationship, schemaRepository);
102+
fullSchemaForIdentifier.Properties[JsonApiPropertyName.Relationship] = referenceSchema.WrapInExtendedSchema();
103+
}
83104
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using Microsoft.OpenApi.Models;
3+
using Swashbuckle.AspNetCore.SwaggerGen;
4+
5+
namespace JsonApiDotNetCore.OpenApi.SchemaGenerators.Components;
6+
7+
internal sealed class ResourceIdSchemaGenerator
8+
{
9+
private readonly SchemaGenerator _defaultSchemaGenerator;
10+
11+
public ResourceIdSchemaGenerator(SchemaGenerator defaultSchemaGenerator)
12+
{
13+
ArgumentGuard.NotNull(defaultSchemaGenerator);
14+
15+
_defaultSchemaGenerator = defaultSchemaGenerator;
16+
}
17+
18+
public OpenApiSchema GenerateSchema(ResourceType resourceType, SchemaRepository schemaRepository)
19+
{
20+
return GenerateSchema(resourceType.IdentityClrType, schemaRepository);
21+
}
22+
23+
public OpenApiSchema GenerateSchema(Type resourceIdClrType, SchemaRepository schemaRepository)
24+
{
25+
ArgumentGuard.NotNull(resourceIdClrType);
26+
ArgumentGuard.NotNull(schemaRepository);
27+
28+
OpenApiSchema idSchema = _defaultSchemaGenerator.GenerateSchema(resourceIdClrType, schemaRepository);
29+
idSchema.Type = "string";
30+
31+
if (resourceIdClrType != typeof(string))
32+
{
33+
// When using string IDs, it's discouraged (but possible) to use an empty string as primary key value, because
34+
// some things won't work: get-by-id, update and delete resource are impossible, and rendered links are unusable.
35+
// For other ID types, provide the length constraint as a fallback in case the type hint isn't recognized.
36+
idSchema.MinLength = 1;
37+
}
38+
39+
return idSchema;
40+
}
41+
}

src/JsonApiDotNetCore.OpenApi/SchemaGenerators/Components/ResourceIdentifierSchemaGenerator.cs

+18-3
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,20 @@ internal sealed class ResourceIdentifierSchemaGenerator
1010
private readonly SchemaGenerator _defaultSchemaGenerator;
1111
private readonly GenerationCacheSchemaGenerator _generationCacheSchemaGenerator;
1212
private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator;
13+
private readonly ResourceIdSchemaGenerator _resourceIdSchemaGenerator;
1314

1415
public ResourceIdentifierSchemaGenerator(SchemaGenerator defaultSchemaGenerator, GenerationCacheSchemaGenerator generationCacheSchemaGenerator,
15-
ResourceTypeSchemaGenerator resourceTypeSchemaGenerator)
16+
ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, ResourceIdSchemaGenerator resourceIdSchemaGenerator)
1617
{
1718
ArgumentGuard.NotNull(defaultSchemaGenerator);
1819
ArgumentGuard.NotNull(generationCacheSchemaGenerator);
1920
ArgumentGuard.NotNull(resourceTypeSchemaGenerator);
21+
ArgumentGuard.NotNull(resourceIdSchemaGenerator);
2022

2123
_defaultSchemaGenerator = defaultSchemaGenerator;
2224
_generationCacheSchemaGenerator = generationCacheSchemaGenerator;
2325
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;
26+
_resourceIdSchemaGenerator = resourceIdSchemaGenerator;
2427
}
2528

2629
public OpenApiSchema GenerateSchema(ResourceType resourceType, bool forRequestSchema, SchemaRepository schemaRepository)
@@ -42,10 +45,22 @@ public OpenApiSchema GenerateSchema(ResourceType resourceType, bool forRequestSc
4245
fullSchemaForIdentifier.Required.Add(JsonApiPropertyName.Id);
4346
}
4447

45-
OpenApiSchema referenceSchemaForResourceType = _resourceTypeSchemaGenerator.GenerateSchema(resourceType, schemaRepository);
46-
fullSchemaForIdentifier.Properties[JsonApiPropertyName.Type] = referenceSchemaForResourceType.WrapInExtendedSchema();
48+
SetResourceType(fullSchemaForIdentifier, resourceType, schemaRepository);
49+
SetResourceId(fullSchemaForIdentifier, resourceType, schemaRepository);
4750
}
4851

4952
return referenceSchemaForIdentifier;
5053
}
54+
55+
private void SetResourceType(OpenApiSchema fullSchemaForIdentifier, ResourceType resourceType, SchemaRepository schemaRepository)
56+
{
57+
OpenApiSchema referenceSchema = _resourceTypeSchemaGenerator.GenerateSchema(resourceType, schemaRepository);
58+
fullSchemaForIdentifier.Properties[JsonApiPropertyName.Type] = referenceSchema.WrapInExtendedSchema();
59+
}
60+
61+
private void SetResourceId(OpenApiSchema fullSchemaForResourceData, ResourceType resourceType, SchemaRepository schemaRepository)
62+
{
63+
OpenApiSchema idSchema = _resourceIdSchemaGenerator.GenerateSchema(resourceType, schemaRepository);
64+
fullSchemaForResourceData.Properties[JsonApiPropertyName.Id] = idSchema;
65+
}
5166
}

src/JsonApiDotNetCore.OpenApi/SchemaGenerators/JsonApiSchemaGenerator.cs

+6-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Reflection;
22
using JsonApiDotNetCore.Controllers;
33
using JsonApiDotNetCore.OpenApi.SchemaGenerators.Bodies;
4+
using JsonApiDotNetCore.OpenApi.SchemaGenerators.Components;
45
using Microsoft.AspNetCore.Mvc.ApiExplorer;
56
using Microsoft.OpenApi.Models;
67
using Swashbuckle.AspNetCore.SwaggerGen;
@@ -9,17 +10,15 @@ namespace JsonApiDotNetCore.OpenApi.SchemaGenerators;
910

1011
internal sealed class JsonApiSchemaGenerator : ISchemaGenerator
1112
{
12-
private static readonly OpenApiSchema IdTypeSchema = new()
13-
{
14-
Type = "string"
15-
};
16-
13+
private readonly ResourceIdSchemaGenerator _resourceIdSchemaGenerator;
1714
private readonly ICollection<BodySchemaGenerator> _bodySchemaGenerators;
1815

19-
public JsonApiSchemaGenerator(IEnumerable<BodySchemaGenerator> bodySchemaGenerators)
16+
public JsonApiSchemaGenerator(ResourceIdSchemaGenerator resourceIdSchemaGenerator, IEnumerable<BodySchemaGenerator> bodySchemaGenerators)
2017
{
18+
ArgumentGuard.NotNull(resourceIdSchemaGenerator);
2119
ArgumentGuard.NotNull(bodySchemaGenerators);
2220

21+
_resourceIdSchemaGenerator = resourceIdSchemaGenerator;
2322
_bodySchemaGenerators = bodySchemaGenerators.ToArray();
2423
}
2524

@@ -31,7 +30,7 @@ public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepos
3130

3231
if (parameterInfo is { Name: "id" } && IsJsonApiParameter(parameterInfo))
3332
{
34-
return IdTypeSchema;
33+
return _resourceIdSchemaGenerator.GenerateSchema(modelType, schemaRepository);
3534
}
3635

3736
BodySchemaGenerator schemaGenerator = GetBodySchemaGenerator(modelType);

src/JsonApiDotNetCore.OpenApi/ServiceCollectionExtensions.cs

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ private static void AddSchemaGenerators(IServiceCollection services)
9595

9696
services.TryAddSingleton<AtomicOperationCodeSchemaGenerator>();
9797
services.TryAddSingleton<ResourceTypeSchemaGenerator>();
98+
services.TryAddSingleton<ResourceIdSchemaGenerator>();
9899
services.TryAddSingleton<MetaSchemaGenerator>();
99100
services.TryAddSingleton<ResourceIdentifierSchemaGenerator>();
100101
services.TryAddSingleton<RelationshipIdentifierSchemaGenerator>();

test/OpenApiKiotaEndToEndTests/AtomicOperations/AtomicCreateResourceTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
121121
Data = new CourseIdentifierInRequest
122122
{
123123
Type = CourseResourceType.Courses,
124-
Id = existingCourse.StringId!
124+
Id = existingCourse.Id
125125
}
126126
},
127127
Student = new ToOneStudentInRequest
@@ -183,7 +183,7 @@ public async Task Can_create_resource_with_client_generated_ID()
183183
Data = new DataInCreateCourseRequest
184184
{
185185
Type = CourseResourceType.Courses,
186-
Id = newCourse.StringId!,
186+
Id = newCourse.Id,
187187
Attributes = new AttributesInCreateCourseRequest
188188
{
189189
Subject = newCourse.Subject

test/OpenApiKiotaEndToEndTests/AtomicOperations/AtomicLocalIdTests.cs

+3-3
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public async Task Can_use_local_IDs()
7272
Data = new DataInCreateCourseRequest
7373
{
7474
Type = CourseResourceType.Courses,
75-
Id = newCourse.StringId!,
75+
Id = newCourse.Id,
7676
Attributes = new AttributesInCreateCourseRequest
7777
{
7878
Subject = newCourse.Subject,
@@ -94,7 +94,7 @@ public async Task Can_use_local_IDs()
9494
new CourseIdentifierInRequest
9595
{
9696
Type = CourseResourceType.Courses,
97-
Id = newCourse.StringId!
97+
Id = newCourse.Id
9898
}
9999
]
100100
},
@@ -130,7 +130,7 @@ public async Task Can_use_local_IDs()
130130
Data = new CourseIdentifierInRequest
131131
{
132132
Type = CourseResourceType.Courses,
133-
Id = newCourse.StringId!
133+
Id = newCourse.Id
134134
}
135135
},
136136
Student = new ToOneStudentInRequest

test/OpenApiKiotaEndToEndTests/AtomicOperations/AtomicRelationshipTests.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
123123
new CourseIdentifierInRequest
124124
{
125125
Type = CourseResourceType.Courses,
126-
Id = existingCourses.ElementAt(0).StringId!
126+
Id = existingCourses.ElementAt(0).Id
127127
},
128128
new CourseIdentifierInRequest
129129
{
130130
Type = CourseResourceType.Courses,
131-
Id = existingCourses.ElementAt(1).StringId!
131+
Id = existingCourses.ElementAt(1).Id
132132
}
133133
]
134134
}
@@ -187,12 +187,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
187187
new CourseIdentifierInRequest
188188
{
189189
Type = CourseResourceType.Courses,
190-
Id = existingCourses.ElementAt(0).StringId!
190+
Id = existingCourses.ElementAt(0).Id
191191
},
192192
new CourseIdentifierInRequest
193193
{
194194
Type = CourseResourceType.Courses,
195-
Id = existingCourses.ElementAt(1).StringId!
195+
Id = existingCourses.ElementAt(1).Id
196196
}
197197
]
198198
}
@@ -250,12 +250,12 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
250250
new CourseIdentifierInRequest
251251
{
252252
Type = CourseResourceType.Courses,
253-
Id = existingTeacher.Teaches.ElementAt(0).StringId!
253+
Id = existingTeacher.Teaches.ElementAt(0).Id
254254
},
255255
new CourseIdentifierInRequest
256256
{
257257
Type = CourseResourceType.Courses,
258-
Id = existingTeacher.Teaches.ElementAt(2).StringId!
258+
Id = existingTeacher.Teaches.ElementAt(2).Id
259259
}
260260
]
261261
}

test/OpenApiKiotaEndToEndTests/AtomicOperations/AtomicUpdateResourceTests.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ await _testContext.RunOnDatabaseAsync(async dbContext =>
203203
Data = new CourseIdentifierInRequest
204204
{
205205
Type = CourseResourceType.Courses,
206-
Id = existingCourse.StringId!
206+
Id = existingCourse.Id
207207
}
208208
},
209209
Student = new ToOneStudentInRequest

0 commit comments

Comments
 (0)