question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Allow Custom Value Object Type Mapping in C# -> Swagger -> C#

See original GitHub issue

I have an API that uses Noda Time types in both input and output. The types are serialized to strings in the JSON using the default Noda Time serialization format (which basically is the ISO-8601 format).

I have an object looking something like this:

public class NodaTimeDataStructure
{
    public System.DateTime DateTime { get; set; }
    public DateInterval DateInterval { get; set; }
    public DateTimeZone DateTimeZone { get; set; }
    public Duration Duration { get; set; }
    public Instant Instant { get; set; }
    public Interval Interval { get; set; }
    public IsoDayOfWeek IsoDayOfWeek { get; set; }
    public LocalDate LocalDate { get; set; }
    public LocalDateTime LocalDateTime { get; set; }
    public LocalTime LocalTime { get; set; }
    public Offset Offset { get; set; }
    public OffsetDate OffsetDate { get; set; }
    public OffsetDateTime OffsetDateTime { get; set; }
    public OffsetTime OffsetTime { get; set; }
    public Period Period { get; set; }
    public ZonedDateTime ZonedDateTime { get; set; }
}

This will normally result in the following Swagger JSON:

"NodaTimeDataStructure": {
  "type": "object",
  "additionalProperties": false,
  "required": [
    "dateTime", "duration", "instant", "interval", "isoDayOfWeek", "localDate", "localDateTime",
    "localTime", "offset", "offsetDate", "offsetDateTime", "offsetTime", "zonedDateTime"
  ],
  "properties": {
    "dateTime":       { "type": "string", "format": "date-time" },
    "instant":        { "type": "string", "format": "date-time" },
    "zonedDateTime":  { "type": "string", "format": "date-time" },
    "offsetDateTime": { "type": "string", "format": "date-time" },
    "localDateTime":  { "type": "string", "format": "date-time" },
    "localDate":      { "type": "string", "format": "date" },
    "localTime":      { "type": "string", "format": "time" },
    "duration":       { "type": "string", "format": "time-span" },
    "dateInterval":   { "type": "array", "items": { "type": "string", "format": "date" } },
    "dateTimeZone":   { "$ref": "#/definitions/DateTimeZone" },
    "interval":       { "$ref": "#/definitions/Interval" },
    "isoDayOfWeek":   { "$ref": "#/definitions/IsoDayOfWeek" },
    "offset":         { "$ref": "#/definitions/Offset" },
    "offsetDate":     { "$ref": "#/definitions/OffsetDate" },
    "offsetTime":     { "$ref": "#/definitions/OffsetTime" },
    "period":         { "$ref": "#/definitions/Period" }
  }
}

This makes it impossible to convert back to the right Noda Time types in a C# client. Apart from the many different types having the exact same format ("date-time") making a mapping impossible, certain types have unfortunate definitions. A DateInterval results in an array of "date", since it’s an enumerable of LocalDate, but a simple start/end date format would work much better. Other methods are created with a $ref to very elaborate objects containing fields of absolutely no interest. Be aware that all of these should be serialized as simple strings (arguably not the intervals).

I am able to create my own Type Mappers and adding them to a AspNetCoreToSwaggerGeneratorSettings like this:

var nodaTimeTypeMappers = new[]
{
    CreateTypeMapper(typeof(DateInterval), "date-interval"),
    CreateTypeMapper(typeof(DateTimeZone), "date-time-zone"),
    CreateTypeMapper(typeof(Duration), "duration"),
    CreateTypeMapper(typeof(Instant), "instant"),
    CreateTypeMapper(typeof(Interval), "interval"),
    CreateTypeMapper(typeof(IsoDayOfWeek), "iso-day-of-week"),
    CreateTypeMapper(typeof(LocalDate), "local-date"),
    CreateTypeMapper(typeof(LocalDateTime), "local-date-time"),
    CreateTypeMapper(typeof(LocalTime), "local-time"),
    CreateTypeMapper(typeof(Offset), "offset"),
    CreateTypeMapper(typeof(OffsetDate), "offset-date"),
    CreateTypeMapper(typeof(OffsetDateTime), "offset-date-time"),
    CreateTypeMapper(typeof(OffsetTime), "offset-time"),
    CreateTypeMapper(typeof(Period), "period"),
    CreateTypeMapper(typeof(ZonedDateTime), "zoned-date-time"),
};

foreach (var typeMapper in nodaTimeTypeMappers)
{
    settings.TypeMappers.Add(typeMapper);
}

PrimitiveTypeMapper CreateTypeMapper(Type type, string name)
{
    return new PrimitiveTypeMapper(type, s =>
    {
        s.Type = JsonObjectType.String;
        s.Format = "noda-time-" + name;
    });
}

to get something like this:

"NodaTimeRequest": {
  "type": "object",
  "additionalProperties": false,
  "required": [
    "dateTime", "duration", "instant", "interval", "isoDayOfWeek", "localDate", "localDateTime",
    "localTime", "offset", "offsetDate", "offsetDateTime", "offsetTime", "zonedDateTime"
  ],
  "properties": {
    "dateTime":       { "type": "string", "format": "date-time" },
    "dateInterval":   { "type": "string", "format": "noda-time-date-interval" },
    "dateTimeZone":   { "type": "string", "format": "noda-time-date-time-zone" },
    "duration":       { "type": "string", "format": "noda-time-duration" },
    "instant":        { "type": "string", "format": "noda-time-instant" },
    "interval":       { "type": "string", "format": "noda-time-interval" },
    "isoDayOfWeek":   { "type": "string", "format": "noda-time-iso-day-of-week" },
    "localDate":      { "type": "string", "format": "noda-time-local-date" },
    "localDateTime":  { "type": "string", "format": "noda-time-local-date-time" },
    "localTime":      { "type": "string", "format": "noda-time-local-time" },
    "offset":         { "type": "string", "format": "noda-time-offset" },
    "offsetDate":     { "type": "string", "format": "noda-time-offset-date" },
    "offsetDateTime": { "type": "string", "format": "noda-time-offset-date-time" },
    "offsetTime":     { "type": "string", "format": "noda-time-offset-time" },
    "period":         { "type": "string", "format": "noda-time-period" },
    "zonedDateTime":  { "type": "string", "format": "noda-time-zoned-date-time" }
  }
}

This allows the formats to be used just like the existing formats ("date-time", "date", "time", "time-span"), but I can’t for the love of God figure out how to make the swagger2csclient use those formats to properly convert back to the corresponding Noda Time types. Am I generally missing something or is this currently not possible?

Issue Analytics

  • State:open
  • Created 4 years ago
  • Reactions:6
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

3reactions
KostaMadorskycommented, May 24, 2020

@lundmikkel have you managed to solve this?

@RicoSuter could you suggest the right way to handle that? Setting Nodatime structs in the Primitive type mapping in NSwagStudio kinda works, however, it also breaks other mapping (e.g. if you set Date Time Type to Instant then all the System.DateTime will be generated as Instant).

1reaction
Pzixelcommented, Jan 26, 2021

I’ve ended up with following converter:

public class NodaLocalDateTimeTypeConverter : TypeConverter
{
	public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
	{
		return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);
	}

	public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
	{
		if (value is string s)
		{
			return LocalDateTime.FromDateTime(DateTime.Parse(s, culture));
		}
		return base.ConvertFrom(context, culture, value);
	}

	public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
	{
		return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
	}
	
	public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
	{
		if (destinationType == typeof(string) && value is LocalDateTime v)
		{
			return LocalDateTimePattern.GeneralIso.Format(v);
		}

		return base.ConvertTo(context, culture, value, destinationType);
	}
}

Which I registered globally at Startup.cs:

TypeDescriptor.AddAttributes(typeof(LocalDateTime), new TypeConverterAttribute(
                typeof(NodaLocalDateTimeTypeConverter)));

Now it passes the check IsPrimitiveType and doesn’t get deconstructed by ApiExplorer so it does pass correctly no NSwag so I see a nice UI in my browser.

Thanks everybody for attention 😃

Read more comments on GitHub >

github_iconTop Results From Across the Web

Change property type as exported by Swagger/Swashbuckle
As you are converting to a non-complex type you should be able to use MapType for this IPAddress example: swagger.
Read more >
Custom Data Types in ASP.NET Core Web APIs
If you have issues with how Swagger generates the documentation for custom types in your ASP.NET Core Web APIs, you should read this...
Read more >
Swagger UI Configuration
Configuration. How to configure. Swagger UI accepts configuration parameters in four locations. From lowest to highest precedence: The swagger-config.yaml ...
Read more >
Configuring and Using Swagger UI in ASP.NET Core Web ...
We are going to learn how to integrate the Swagger UI/OpenAPI in an ASP.NET Core Web API, extend the documentation, and customize UI....
Read more >
Parameter Serialization
Parameter Serialization · style defines how multiple values are delimited. Possible styles depend on the parameter location – path, query, header or cookie....
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found