New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
How to map Optional<T> #2359
Comments
Is there anyone who can help? |
Same problem here for a custom I think if |
I managed to handle it using a |
@KillerBoogie were you able to resolve this? I'm trying to use Optional for a patch request too. |
not, yet. I haven't had time to try out the hint from momt99. |
@momt99 any chance you could share the snippet here? thanks |
@maganuk public class PaginatedListSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var type = context.Type;
if (!(type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(PaginatedList<>)))
{
return;
}
schema.Type = string.Empty;
schema.Items = null;
// For properties: PageIndex, PageSize, ...
var paginatedListSchema = context.SchemaGenerator.GenerateSchema(
typeof(PaginatedList), context.SchemaRepository);
schema.AllOf.Add(paginatedListSchema);
var itemType = type.GetGenericArguments()[0];
var genericPartType = new OpenApiSchema();
genericPartType.Properties.Add(
ToCamelCase(nameof(PaginatedList.Items)),
new OpenApiSchema
{
Type = "array",
Items = context.SchemaGenerator.GenerateSchema(
itemType, context.SchemaRepository),
});
schema.AllOf.Add(genericPartType);
}
private static string ToCamelCase(string name) =>
name.Length > 1
? char.ToLower(name[0]) + name[1..]
: name.ToLower();
private interface PaginatedList
{
IReadOnlyList<object> Items { get; }
int PageIndex { get; }
int PageSize { get; }
int TotalCount { get; }
int TotalPages { get; }
bool HasPreviousPage { get; }
bool HasNextPage { get; }
}
} |
@momt99 thanks very much for this! |
@maganuk or @KillerBoogie, did you ever figure out a solution for The closes I've gotten to is here: https://github.com/harvzor/harvzor-optional/pull/3/files#diff-5ab2141080f58ecf05c182fbed6f6531819aeaab1c5034af92d5336dfe146fb9R86 But the code is a real mess and is likely full of bugs. I'm trying to create a library which will solve this Optional issue everywhere (turns out there's lots of caveats with Newtonsoft/STJ if you're not careful). |
@harvzor we ended up creating separate models without the Optional generics and using those for Swagger. Irritating but works. In fact I tried again recently to map the optionals but the problem is that reflection does not reveal if the inner type is nullable or not and we the nullability status to be present in our Swagger doc. I think the best way to solve this is using code generators. But we don't have the bandwidth to work on that. Please do let me know if you make any progress on this. |
We have something like this: internal class OptionalDocumentFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var schema in swaggerDoc.Components.Schemas)
{
RemoveOptionalLevelFromProperties(schema.Value, context);
}
RemoveOptionalSchemas(swaggerDoc);
}
private void RemoveOptionalLevelFromProperties(OpenApiSchema schema, DocumentFilterContext context)
{
if (schema.Properties == null)
{
return;
}
foreach (var property in schema.Properties.ToList())
{
if (property.Value.AllOf != null)
{
// swashbuckle uses allOf for references, so that custom coments can be included.
for (int i = 0; i < property.Value.AllOf.Count; i++)
{
var currentSchema = property.Value.AllOf[i];
if (currentSchema.IsReferenceToOptional(context.SchemaRepository))
{
var optionalSchema = context.SchemaRepository.Schemas[currentSchema.Reference.Id];
if (!optionalSchema.TryGetValuePropertySchema(out var valueSchema))
{
throw new InvalidOperationException("Optional schema must have a value property.");
}
if (valueSchema.Reference != null)
{
// if the value of optional is a reference (i.e. a complex type), then just use it as all off.
property.Value.AllOf[i] = valueSchema;
}
else
{
// this is e.g. Optional<string>. We can't use AllOf here, so we must replace the whole property.
schema.Properties[property.Key] = valueSchema;
}
}
}
}
}
}
private void RemoveOptionalSchemas(OpenApiDocument swaggerDoc)
{
var schemasToRemove = swaggerDoc.Components.Schemas
.Where(e => e.IsOptional())
.ToList();
foreach (var schema in schemasToRemove)
{
swaggerDoc.Components.Schemas.Remove(schema);
}
}
} And enable like this: options.UseAllOfToExtendReferenceSchemas();
options.DocumentFilter<OptionalDocumentFilter>(); |
@macwier we're just trying this out. Could you possibly share what you did for IsReferenceToOptional and TryGetValuePropertySchema? |
Both of this depend on your implementation of the The In essence what the whole thing does is it replaces any occurence of |
I used @macwier's code and adjusted it to work with public class OptionalDocumentFilter : IDocumentFilter
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
foreach (var schema in swaggerDoc.Components.Schemas)
{
RemoveOptionalLevelFromProperties(schema.Value, context);
}
RemoveOptionalSchemas(swaggerDoc);
}
private void RemoveOptionalLevelFromProperties(OpenApiSchema schema, DocumentFilterContext context)
{
if (schema.Properties == null)
{
return;
}
foreach (var property in schema.Properties.ToList())
{
if (property.Value.AllOf != null)
{
// swashbuckle uses allOf for references, so that custom coments can be included.
for (int i = 0; i < property.Value.AllOf.Count; i++)
{
var currentSchema = property.Value.AllOf[i];
if (IsReferenceToOptional(currentSchema.Reference.Id, context.SchemaRepository))
{
var optionalSchema = context.SchemaRepository.Schemas[currentSchema.Reference.Id];
if (!optionalSchema.Properties.TryGetValue("value", out var valueSchema))
{
throw new InvalidOperationException("Optional schema must have a value property.");
}
if (valueSchema.Reference != null)
{
// if the value of optional is a reference (i.e. a complex type), then just use it as all off.
property.Value.AllOf[i] = valueSchema;
}
else
{
// this is e.g. Optional<string>. We can't use AllOf here, so we must replace the whole property.
schema.Properties[property.Key] = valueSchema;
}
}
}
}
}
}
private static bool IsReferenceToOptional(string referenceId, SchemaRepository schemaRepository)
{
var referencedSchema = schemaRepository.Schemas.First(x => x.Key == referenceId);
return IsOptionalSchema(referencedSchema);
}
private static bool IsOptionalSchema(KeyValuePair<string, OpenApiSchema> referencedSchema)
{
return referencedSchema.Key.EndsWith("Optional") &&
referencedSchema.Value.Properties.Count() == 2 &&
referencedSchema.Value.Properties.Any(x => x.Key == "value") &&
referencedSchema.Value.Properties.Any(x => x.Key == "hasValue");
}
private void RemoveOptionalSchemas(OpenApiDocument swaggerDoc)
{
swaggerDoc.Components.Schemas
.Where(IsOptionalSchema)
.ToList()
.ForEach(schema => swaggerDoc.Components.Schemas.Remove(schema));
}
} |
… to show it doesn't work 100%.
I've given this a quick test and it misses some pain points in the generated schema: Doesn't work with Optional query paramsThis wouldn't work on query params, like if you had a method like: Only removes Optional schemas, and not other schemas if they shouldn't be generatedThis filter only removes any schemas with the name ending in
SwashBuckle will generate a schema for the And even then, doesn't seem to actually remove Optional schemas?I tried a fairly simple example:
And it never actually removed the
Maybe because it's searching for properties on schemas? Probably lots of other issuesLikely it's good enough for basic use cases, but you'll find it a bit buggy if you try many other things, so this isn't a perfect workaround. My testsI implemented this and ran this against the project test I've written for my fix. You can see the failing tests here: https://github.com/harvzor/harvzor-optional/actions/runs/6110092503/job/16582434597#step:4:142 And here's the related PR where I implemented your code: harvzor/harvzor-optional#5 If you run the |
I've created a PR to better support open generics: #2704 Would be really nice if @alesdvorakcz, @macwier and @maganuk (and anyone else) could take a look to see if it solves the issue of open generics 🙏 |
@harvzor I think what you are suggesting in the PR is exactly what I would need. I am currently working with an API that uses With your PR I believe I could make a mapping like this: c.MapType(typeof(Option<>), (ctx) =>
{
var type = ctx.UnderlyingType.GetGenericArguments[0];
var schema = ctx.SchemaGenerator.GenerateSchema(type, ctx.SchemaRepository);
schema.Nullable = true;
return schema;
} I can see the issue and PR has been idle since September. Is there a hold-up? When do you think this could be added? |
I also need a way to provide a custom mapping for a type in terms of some other types. Without this feature Would love for this PR to make some progress (willing to help fwiw). |
Please excuse, if this is not the right spot to post. It is unclear if I just don't know how to use the api correctly, if swashbuckle is missing a feature, or if it is a bug.
I'm using
Optional<T>
in my DTOs to implement Json Patch (details). The type T is stored in the 'Value' field of theOptional<T>
class. The Optional class allows to differentiate between null and not provided.The first DTOs I worked with held only primitive types, e.g.
The example that Swagger UI displays is wrong, because it displays the property "value":
Our solution was to map the types in Startup.cs.
The example is then correctly displayed as:
In my latest DTO I needed nested objects. The simplified DTO looks like this:
The example will wrongly show the nested value property again:
List<string>
can be mapped with:I have problems to map the nested objects. I tried:
But this only shows empty braces
{}
. The way I understand the api I would need to manually define all the types of the AddressDTO withItem = new OpenApiSchema{...}
. Is there any solution that can just refer to the AddressDTO?It is in general cumbersome to define a MapType for every T that is used inside Optional. Is there any way to have a generic mapping from Optional to T? Something like:
The text was updated successfully, but these errors were encountered: