openapi-generator icon indicating copy to clipboard operation
openapi-generator copied to clipboard

[BUG] [C#] Optional parameters prevent deserialization

Open marcselman opened this issue 1 year ago • 2 comments

Description

I'm using version 7.5.0 of the generator and I'm generating C# classes using the generichost library setting.

Suppose I have the following OpenAPI component schema:

"UserDiscount": {
  "type": "object",
  "required": [
    "id",
    "name"
  ],
  "properties": {
    "id": {
      "$ref": "#/components/schemas/uuid"
    },
    "name": {
      "type": "string"
    }
    "markup": {
      "type": "number",
      "format": "decimal"
    }
  }
}

It will then generate the following C# class:

public partial class UserDiscount : IValidatableObject
{
	[JsonConstructor]
	public UserDiscount(string name, Guid id, Option<decimal?> discount = default)
	{
		Name = name;
		Id = id;
		DiscountOption = discount;
		OnCreated();
	}

	partial void OnCreated();

	/// <summary>
	/// Gets or Sets BrandCode
	/// </summary>
	[JsonPropertyName("name")]
	public string Name { get; set; }

	/// <summary>
	/// Gets or Sets Id
	/// </summary>
	[JsonPropertyName("id")]
	public Guid Id { get; set; }

	/// <summary>
	/// Used to track the state of Discount
	/// </summary>
	[JsonIgnore]
	[System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)]
	public Option<decimal?> DiscountOption { get; private set; }

	/// <summary>
	/// Gets or Sets Discount
	/// </summary>
	[JsonPropertyName("discount")]
	public decimal? Discount { get { return this.DiscountOption; } set { this.DiscountOption = new(value); } }

	/// <summary>
	/// Gets or Sets additional properties
	/// </summary>
	[JsonExtensionData]
	public Dictionary<string, JsonElement> AdditionalProperties { get; } = new Dictionary<string, JsonElement>();

	/// <summary>
	/// Returns the string presentation of the object
	/// </summary>
	/// <returns>String presentation of the object</returns>
	public override string ToString()
	{
		StringBuilder sb = new StringBuilder();
		sb.Append("class CategoryInfo {\n");
		sb.Append("  Name: ").Append(Name).Append("\n");
		sb.Append("  Id: ").Append(Id).Append("\n");
		sb.Append("  Discount: ").Append(Discount).Append("\n");
		sb.Append("  AdditionalProperties: ").Append(AdditionalProperties).Append("\n");
		sb.Append("}\n");
		return sb.ToString();
	}

	/// <summary>
	/// To validate all properties of the instance
	/// </summary>
	/// <param name="validationContext">Validation context</param>
	/// <returns>Validation Result</returns>
	IEnumerable<System.ComponentModel.DataAnnotations.ValidationResult> IValidatableObject.Validate(ValidationContext validationContext)
	{
		yield break;
	}
}

But, when trying to deserialize some data like this:

var data = @"{
	""name"": ""someName"",
	""id"": ""527bce01-4b7a-41ed-b51d-5608acb1549c"",
	""discount"": 4
}";

JsonSerializer.Deserialize<UserDiscount>(data).Dump();

I get the following error:

InvalidOperationException: Each parameter in the deserialization constructor on type 'UserDiscount' must bind to an object property or field on deserialization. Each parameter name must match with a property or field on the object. Fields are only considered when 'JsonSerializerOptions.IncludeFields' is enabled. The match can be case-insensitive.

The problem is that the discount parameter of the constructor is of type Option<decimal?> while the discount property is of type decimal?. If I rename the constructor parameter to discountOption it works, because it maps to a property with the same name and type. Or if I change the constructor parameter type to decimal? it works as well, because that also maps to a property with the same name and type.

I think the constructor parameter type does not need to be Option<decimal?> but can simply be decimal? since it will be implicitly converted anyway.

openapi-generator version

7.5.0

marcselman avatar May 24 '24 09:05 marcselman

Why does it generate a Option<decimal?> anyways?

patrickdejong avatar May 24 '24 09:05 patrickdejong

It does that if it is both not required and nullable. I see in the json you gave here, the field is called markup, yet the C# property is called discount. Can you please verify what you have is not working? When you de/serialize, ensure you're using the json options as well.

devhl-labs avatar May 25 '24 01:05 devhl-labs

Yeah, I simplified the demo code a bit and removed the wrong name. It should have been discount, I've updated it now.

Your comment about the json options pointed me in the right direction! I totally missed that I needed the JsonConverter that's generated as well. 😖 After registrering that the serialization works fine.

Thanks for your help. I'll close this ticket.

For anyone else interested, this is the correct way te deserialize:

var serializeOptions = new JsonSerializerOptions
{
	Converters =
	{
		new UserDiscountJsonConverter()
	}
};
JsonSerializer.Deserialize<UserDiscount>(data, serializeOptions).Dump();

marcselman avatar May 26 '24 10:05 marcselman

Yes but the options are already instantiated and in the service provider. You don't have to create another instance.

devhl-labs avatar May 26 '24 14:05 devhl-labs