diff --git a/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache b/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache index ecae5ef6e026..73b3e486cf88 100644 --- a/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache +++ b/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache @@ -82,10 +82,10 @@ class {{{classname}}} { {{#pattern}} json[r'{{{baseName}}}'] = value == null ? null : (_isEpochMarker(r'{{{pattern}}}') ? value.millisecondsSinceEpoch - : _dateFormatter.format(value.toUtc())); + : _dateFormatter.format(value)); {{/pattern}} {{^pattern}} - json[r'{{{baseName}}}'] = value == null ? null : _dateFormatter.format(value.toUtc()); + json[r'{{{baseName}}}'] = value == null ? null : _dateFormatter.format(value); {{/pattern}} {{/isDate}} {{^isDateTime}} @@ -120,10 +120,10 @@ class {{{classname}}} { {{#pattern}} json[r'{{{baseName}}}'] = _isEpochMarker(r'{{{pattern}}}') ? this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.millisecondsSinceEpoch - : _dateFormatter.format(this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.toUtc()); + : _dateFormatter.format(this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}); {{/pattern}} {{^pattern}} - json[r'{{{baseName}}}'] = _dateFormatter.format(this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}.toUtc()); + json[r'{{{baseName}}}'] = _dateFormatter.format(this.{{{name}}}{{#isNullable}}!{{/isNullable}}{{^isNullable}}{{^required}}{{^defaultValue}}!{{/defaultValue}}{{/required}}{{/isNullable}}); {{/pattern}} {{/isDate}} {{^isDateTime}} diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartClientCodegenTest.java index 357f6a9a72f5..2215908d3d65 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/dart/DartClientCodegenTest.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Map; public class DartClientCodegenTest { @@ -101,6 +102,10 @@ public void testEnumPropertyWithQuotes() { } private List generateDartNativeFromSpec(String specPath) throws Exception { + return generateDartNativeFromSpec(specPath, Map.of()); + } + + private List generateDartNativeFromSpec(String specPath, Map additionalProperties) throws Exception { File output = Files.createTempDirectory("dart-native-test").toFile(); output.deleteOnExit(); @@ -108,6 +113,7 @@ private List generateDartNativeFromSpec(String specPath) throws Exception .setGeneratorName("dart") .setInputSpec(specPath) .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + additionalProperties.forEach(configurator::addAdditionalProperty); ClientOptInput opts = configurator.toClientOptInput(); DefaultGenerator generator = new DefaultGenerator(); @@ -199,4 +205,43 @@ public void testNullableNestedComplexArraysPreserveNullEntries() throws Exceptio TestUtils.assertFileContains(modelFile.toPath(), "e == null ? null : NullableRequiredModel.listFromJson(e)"); } + + // DateFormat('yyyy-MM-dd') does no timezone conversion, it just formats the y/m/d the + // DateTime already carries. So toUtc() only shifts the value across a day boundary and + // the date comes out off by one - back a day east of UTC, forward a day west of it. + @Test(description = "format: date must not be converted to UTC before formatting") + public void testDateOnlyFieldsAreNotConvertedToUtc() throws Exception { + List files = generateDartNativeFromSpec( + "src/test/resources/3_0/dart/dart-native-deserialization-bugs.yaml"); + + File modelFile = files.stream() + .filter(f -> f.getName().equals("date_only_model.dart")) + .findFirst() + .orElseThrow(() -> new AssertionError("date_only_model.dart not found in generated files")); + + TestUtils.assertFileContains(modelFile.toPath(), + "json[r'requiredDate'] = _dateFormatter.format(this.requiredDate);"); + TestUtils.assertFileContains(modelFile.toPath(), + "json[r'optionalDate'] = _dateFormatter.format(this.optionalDate!);"); + // A pattern routes it through the _isEpochMarker ternary, non-epoch side formats the same way + TestUtils.assertFileContains(modelFile.toPath(), + ": _dateFormatter.format(this.patternedDate!);"); + } + + @Test(description = "format: date must not be converted to UTC in the Optional path either") + public void testDateOnlyFieldsAreNotConvertedToUtcWithUseOptional() throws Exception { + List files = generateDartNativeFromSpec( + "src/test/resources/3_0/dart/dart-native-deserialization-bugs.yaml", + Map.of("useOptional", true)); + + File modelFile = files.stream() + .filter(f -> f.getName().equals("date_only_model.dart")) + .findFirst() + .orElseThrow(() -> new AssertionError("date_only_model.dart not found in generated files")); + + TestUtils.assertFileContains(modelFile.toPath(), + "json[r'optionalDate'] = value == null ? null : _dateFormatter.format(value);"); + TestUtils.assertFileContains(modelFile.toPath(), + ": _dateFormatter.format(value));"); + } } diff --git a/modules/openapi-generator/src/test/resources/3_0/dart/dart-native-deserialization-bugs.yaml b/modules/openapi-generator/src/test/resources/3_0/dart/dart-native-deserialization-bugs.yaml index 53f2172adee4..218af8dfe017 100644 --- a/modules/openapi-generator/src/test/resources/3_0/dart/dart-native-deserialization-bugs.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/dart/dart-native-deserialization-bugs.yaml @@ -47,3 +47,18 @@ components: nullable: true items: $ref: '#/components/schemas/NullableRequiredModel' + DateOnlyModel: + type: object + required: + - requiredDate + properties: + requiredDate: + type: string + format: date + optionalDate: + type: string + format: date + patternedDate: + type: string + format: date + pattern: '^\d{4}-\d{2}-\d{2}$' diff --git a/samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/format_test.dart b/samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/format_test.dart index 5c22fff27825..3a5506f6d561 100644 --- a/samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/format_test.dart +++ b/samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/format_test.dart @@ -233,7 +233,7 @@ class FormatTest { } else { json[r'binary'] = null; } - json[r'date'] = _dateFormatter.format(this.date.toUtc()); + json[r'date'] = _dateFormatter.format(this.date); if (this.dateTime != null) { json[r'dateTime'] = this.dateTime!.toUtc().toIso8601String(); } else { diff --git a/samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/nullable_class.dart b/samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/nullable_class.dart index 7dc16ebaf5bc..8742cbe2ccd8 100644 --- a/samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/nullable_class.dart +++ b/samples/openapi3/client/petstore/dart2/petstore_client_lib_fake/lib/model/nullable_class.dart @@ -108,7 +108,7 @@ class NullableClass { json[r'string_prop'] = null; } if (this.dateProp != null) { - json[r'date_prop'] = _dateFormatter.format(this.dateProp!.toUtc()); + json[r'date_prop'] = _dateFormatter.format(this.dateProp!); } else { json[r'date_prop'] = null; }