gibahjoe / openapi-generator-dart

Openapi Generator for Dart/Flutter
BSD 3-Clause "New" or "Revised" License
128 stars 34 forks source link

Null field for propertyName when discriminator used in deserializer #167

Open andreykud opened 1 day ago

andreykud commented 1 day ago

Description of the bug

When discriminator used in api specification it seems common fields for inherited objects not filled in dart generator implementation.

It seems generated dart code does not calls method named '_deserializeProperties' in method 'deserialize' (in test.dart). So in generated test.g.dart when _build calls it throws an Exception, because type was not parsed and it is null.

Manual adding call this method to generated code fixes issue

Steps to reproduce

Generate code by specification below

Minimal openapi specification

test-contract.yml

openapi: 3.0.3
info:
  version: '1.0'
  title: Test
paths:
  /test:
    get:
      operationId: test
      responses:
        '200':
          description: response
          content:
            application/json:
              schema:
                $ref: "./test.yml"

test.yml:

type: object
oneOf:
  - $ref: './a.yml'
  - $ref: './b.yml'
discriminator:
  propertyName: type
required:
  - type
properties:
  type:
    $ref: './testType.yml'

testType.yml:

type: string
enum:
  - a
  - b

a.yml:

type: object
properties:
  optionA1:
    type: string

b.yml:

type: object
properties:
  optionB1:
    type: string

Annotation used

@Openapi( additionalProperties: AdditionalProperties( pubName: 'test_api', ), inputSpec: InputSpec(path: '../../../common/contract/test-contract.yml'), generatorName: Generator.dio, runSourceGenOnOutput: true, outputDirectory: '../generated/test_api')

Expected behavior

Generator should generate without errors

Logs

No response

Screenshots

No response

Platform

Windows

Library version

6.0.0

Flutter version

3.24.3

Flutter channel

stable

Additional context

No response

gibahjoe commented 22 hours ago

Hi, I just ran the generator on the spec you provided and there are no compile-time errors in the generated code.

is the error you are getting a runtime error?

if so, could you post the generated code and the modified code? This will help in understanding what the issue is.

andreykud commented 21 hours ago

I also don't have any errors on compile, but i have an error on deserialization.

Example string is:

{"type": "a", "optionA1":"optionA1"}
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//

// ignore_for_file: unused_element
import 'package:test_deserialize_api/src/model/a.dart';
import 'package:test_deserialize_api/src/model/b.dart';
import 'package:test_deserialize_api/src/model/test_type.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
import 'package:one_of/one_of.dart';

part 'test_deserialize.g.dart';

/// TestDeserialize
///
/// Properties:
/// * [type]
/// * [optionA1]
/// * [optionB1]
@BuiltValue()
abstract class TestDeserialize
    implements Built<TestDeserialize, TestDeserializeBuilder> {
  @BuiltValueField(wireName: r'type')
  TestType get type;
  // enum typeEnum {  a,  b,  };

  /// One Of [A], [B]
  OneOf get oneOf;

  static const String discriminatorFieldName = r'type';

  static const Map<String, Type> discriminatorMapping = {
    r'a': A,
    r'b': B,
  };

  TestDeserialize._();

  factory TestDeserialize([void updates(TestDeserializeBuilder b)]) =
      _$TestDeserialize;

  @BuiltValueHook(initializeBuilder: true)
  static void _defaults(TestDeserializeBuilder b) => b;

  @BuiltValueSerializer(custom: true)
  static Serializer<TestDeserialize> get serializer =>
      _$TestDeserializeSerializer();
}

extension TestDeserializeDiscriminatorExt on TestDeserialize {
  String? get discriminatorValue {
    if (this is A) {
      return r'a';
    }
    if (this is B) {
      return r'b';
    }
    return null;
  }
}

extension TestDeserializeBuilderDiscriminatorExt on TestDeserializeBuilder {
  String? get discriminatorValue {
    if (this is ABuilder) {
      return r'a';
    }
    if (this is BBuilder) {
      return r'b';
    }
    return null;
  }
}

class _$TestDeserializeSerializer
    implements PrimitiveSerializer<TestDeserialize> {
  @override
  final Iterable<Type> types = const [TestDeserialize, _$TestDeserialize];

  @override
  final String wireName = r'TestDeserialize';

  Iterable<Object?> _serializeProperties(
    Serializers serializers,
    TestDeserialize object, {
    FullType specifiedType = FullType.unspecified,
  }) sync* {
    yield r'type';
    yield serializers.serialize(
      object.type,
      specifiedType: const FullType(TestType),
    );
  }

  @override
  Object serialize(
    Serializers serializers,
    TestDeserialize object, {
    FullType specifiedType = FullType.unspecified,
  }) {
    final oneOf = object.oneOf;
    final result =
        _serializeProperties(serializers, object, specifiedType: specifiedType)
            .toList();
    result.addAll(serializers.serialize(oneOf.value,
        specifiedType: FullType(oneOf.valueType)) as Iterable<Object?>);
    return result;
  }

  void _deserializeProperties(
    Serializers serializers,
    Object serialized, {
    FullType specifiedType = FullType.unspecified,
    required List<Object?> serializedList,
    required TestDeserializeBuilder result,
    required List<Object?> unhandled,
  }) {
    for (var i = 0; i < serializedList.length; i += 2) {
      final key = serializedList[i] as String;
      final value = serializedList[i + 1];
      switch (key) {
        case r'type':
          final valueDes = serializers.deserialize(
            value,
            specifiedType: const FullType(TestType),
          ) as TestType;
          result.type = valueDes;
          break;
        default:
          unhandled.add(key);
          unhandled.add(value);
          break;
      }
    }
  }

  @override
  TestDeserialize deserialize(
    Serializers serializers,
    Object serialized, {
    FullType specifiedType = FullType.unspecified,
  }) {
    final result = TestDeserializeBuilder();
    Object? oneOfDataSrc;
    final serializedList = (serialized as Iterable<Object?>).toList();
    final discIndex =
        serializedList.indexOf(TestDeserialize.discriminatorFieldName) + 1;
    final discValue = serializers.deserialize(serializedList[discIndex],
        specifiedType: FullType(String)) as String;
    oneOfDataSrc = serialized;
    final oneOfTypes = [
      A,
      B,
    ];
    Object oneOfResult;
    Type oneOfType;
    switch (discValue) {
      case r'a':
        oneOfResult = serializers.deserialize(
          oneOfDataSrc,
          specifiedType: FullType(A),
        ) as A;
        oneOfType = A;
        break;
      case r'b':
        oneOfResult = serializers.deserialize(
          oneOfDataSrc,
          specifiedType: FullType(B),
        ) as B;
        oneOfType = B;
        break;
      default:
        throw UnsupportedError(
            "Couldn't deserialize oneOf for the discriminator value: ${discValue}");
    }
    result.oneOf = OneOfDynamic(
        typeIndex: oneOfTypes.indexOf(oneOfType),
        types: oneOfTypes,
        value: oneOfResult);
    return result.build();
  }
}

Code, fixes issue is just adding call '_deserializeProperties' to the deserialize method:

    final unhandled = <Object?>[];
    _deserializeProperties(
      serializers,
      serialized,
      specifiedType: specifiedType,
      serializedList: serializedList,
      unhandled: unhandled,
      result: result,
    );
andreykud commented 46 minutes ago

Hi, I also have one follow up question.

What is correct way to create object Test from example above?

Now I can create TestBuilder class and fill field type. But how correctly fill inherited objects parameters (for example parameter optionA1 for Inherited object A)? TestBuilder has field oneOf. How it should be filled?