NSwag icon indicating copy to clipboard operation
NSwag copied to clipboard

TypeScript inheritance constructors is not created correctly

Open madsmadsen opened this issue 3 years ago • 5 comments

We are experiencing issues with creating new instances of classes with inheritance, the properties of the base class is set correctly, but the class itself's properties are undefined.

The issues lays in the constructor in the class does not set it's own properties, but instead relies on the base class to have set all properties. Now when using babel, any class constructor will initialize any properties set outside the constructor.

Now from the following generated swagger.json (Swagger 2.0):

{
  "x-generator": "NSwag v13.10.8.0 (NJsonSchema v10.4.4.0 (Newtonsoft.Json v12.0.0.0))",
  "swagger": "2.0",
  "host": "blah.testing.tst",
  "schemes": [
    "https"
  ],
  "AnswerRequest": {
      "type": "object",
      "discriminator": "swagger_discriminator",
      "x-abstract": true,
      "required": [
        "order",
        "stepId",
        "swagger_discriminator"
      ],
      "properties": {
        "choiceId": {
          "type": "string",
          "format": "guid"
        },
        "order": {
          "type": "integer",
          "format": "int32"
        },
        "stepId": {
          "type": "string",
          "format": "guid"
        },
        "stepComment": {
          "type": "string"
        },
        "stepTitle": {
          "type": "string"
        },
        "swagger_discriminator": {
          "type": "string"
        }
      }
    },
    "TextAnswerRequest": {
      "type": "object",
      "required": [
        "choiceOrder",
        "choiceScore",
        "multiSelectEnabled",
        "scoringEnabled"
      ],
      "properties": {
        "choiceOrder": {
          "type": "integer",
          "format": "int32"
        },
        "choiceScore": {
          "type": "number",
          "format": "float"
        },
        "choiceTitle": {
          "type": "string"
        },
        "multiSelectEnabled": {
          "type": "boolean"
        },
        "scoringEnabled": {
          "type": "boolean"
        }
      },
      "allOf": [
        {
          "$ref": "#/definitions/AnswerRequest"
        }
      ]
    },
...snip
}

These request classes are generated with the following constructors;

export abstract class AnswerRequest implements IAnswerRequest {
    choiceId?: string | null;
    order!: number;
    stepId!: string;
    stepComment?: string | null;
    stepTitle?: string | null;

    protected _discriminator: string;

    constructor(data?: IAnswerRequest) {
        if (data) {
            for (var property in data) {
                if (data.hasOwnProperty(property))
                    (<any>this)[property] = (<any>data)[property];
            }
        }
        this._discriminator = "AnswerRequest";
    }
}

export class TextAnswerRequest extends AnswerRequest implements ITextAnswerRequest {
    choiceOrder!: number;
    choiceScore!: number;
    choiceTitle?: string | null;
    multiSelectEnabled!: boolean;
    scoringEnabled!: boolean;

    constructor(data?: ITextAnswerRequest) {
        super(data);
        this._discriminator = "TextAnswerRequest";
    }
}

But I would expect the classes to generate the following constructors;

export abstract class AnswerRequest implements IAnswerRequest {
    choiceId?: string | null;
    order!: number;
    stepId!: string;
    stepComment?: string | null;
    stepTitle?: string | null;

    protected _discriminator: string;

    constructor(data?: IAnswerRequest) {
        if (data) {
            this.choiceId = data["choiceId"] !== undefined ? data["choiceId"] : <any>null;
            this.order = data["order"] !== undefined ? data["order"] : <any>null;
            this.stepId = data["stepId"] !== undefined ? data["stepId"] : <any>null;
            this.stepComment = data["stepComment"] !== undefined ? data["stepComment"] : <any>null;
            this.stepTitle = data["stepTitle"] !== undefined ? data["stepTitle"] : <any>null;
        }
        this._discriminator = "AnswerRequest";
    }
}

export class TextAnswerRequest extends AnswerRequest implements ITextAnswerRequest {
    choiceOrder!: number;
    choiceScore!: number;
    choiceTitle?: string | null;
    multiSelectEnabled!: boolean;
    scoringEnabled!: boolean;

    constructor(data?: ITextAnswerRequest) {
        super(data);
        if (data) {
            this.choiceOrder = data["choiceOrder"] !== undefined ? data["choiceOrder"] : <any>null;
            this.choiceScore = data["choiceScore"] !== undefined ? data["choiceScore"] : <any>null;
            this.choiceTitle = data["choiceTitle"] !== undefined ? data["choiceTitle"] : <any>null;
            this.multiSelectEnabled = data["multiSelectEnabled"] !== undefined ? data["multiSelectEnabled"] : <any>null;
            this.scoringEnabled = data["scoringEnabled"] !== undefined ? data["scoringEnabled"] : <any>null;
        }
        this._discriminator = "TextAnswerRequest";
    }
}

We can see from https://github.com/RicoSuter/NJsonSchema/wiki/Inheritance that this should also be the expected result, this is however not the case.

Our current workaround is to use fromJS(data?: any) but we lose type safety because of the any.

madsmadsen avatar Aug 03 '21 07:08 madsmadsen

We are facing this issue as well.

@RicoSuter, I'm ready to pull-request it, but want to double check how it should look like.

It could be like in base class (with additional check for this.hasOwnProperty:

    constructor(data?: ITextAnswerRequest) {
        super(data);
        if (data) {
            for (var property in data) {
                if (data.hasOwnProperty(property) && this.hasOwnProperty(property))
                    (<any>this)[property] = (<any>data)[property];
            }
       }

is it ok?

Shaddix avatar Aug 13 '21 14:08 Shaddix

I can agree that problem still exists in the newest verison. When it comes to derived object initialization, all properties are initialized in ancestor class and then in derived are missing.

I hope @Shaddix solution should work well (i've tested it in my files). Anyway, changes has to be made in https://github.com/RicoSuter/NJsonSchema

I'm also courious how samples in https://github.com/RicoSuter/NJsonSchema/wiki/Inheritance was generated because according to the file: https://github.com/RicoSuter/NJsonSchema/blob/master/src/NJsonSchema.CodeGeneration.TypeScript/Templates/Class.liquid it's impossible (and was impossible) to generate such code: image

jposert avatar Jan 17 '22 14:01 jposert

We are also experiencing exactly the same behaviour as @madsmadsen described. Is there any update on this?

vocko avatar Jun 22 '22 03:06 vocko

We are experiencing the same behavior. Any update on this? Or workaround?

OptimusPi avatar Jul 27 '22 19:07 OptimusPi

@vocko @OptimusPi I did a PR (https://github.com/RicoSuter/NJsonSchema/pull/1485), anyway RicoSuter got right, and that's not a fault of the library - the problem is related to the react-build itself (or babel/webpack i should say).

Because i havent found any good generic solution that may be applied to everyone, i figured out a workaround that works fine for me.
The change shoud be made to the Class.liquid file I mentioned before - and there's an option to override the default behaviour.

Just put the file into some directory and add the directory to the templateDirectory option in .nswag file. The template will override the default one and should generate proper code.

Class.liquid.zip

jposert avatar Jul 27 '22 19:07 jposert

Hi everyone, so I ended up resolving this in a slightly different fashion, but it works for me, so I figured I would share.

I made a subclass of the TypeScriptClientGenerator class e.g.


    public class MyTypeScriptGenerator : TypeScriptClientGenerator
    {
        public MyTypeScriptGenerator(OpenApiDocument document, TypeScriptClientGeneratorSettings settings) : base(document, settings)
        {
        }

        protected override IEnumerable<CodeArtifact> GenerateDtoTypes()
        {
            var types = base.GenerateDtoTypes();
            foreach (var t in types)
            {
                if (t.BaseTypeName == "MyBaseClass")
                {
                    var ts = t.Code;
                    var fixedTs = ts.Replace("super(data);\n", "super(data);\n        if (data) {\n            for (var property in data) {\n                if (data.hasOwnProperty(property))\n                    (<any>this)[property] = (<any>data)[property];\n            }\n        }\n");
                    yield return new CodeArtifact(t.TypeName, t.BaseTypeName, CodeArtifactType.Class, CodeArtifactLanguage.TypeScript, CodeArtifactCategory.Contract, fixedTs);
                }
                else
                {
                    yield return t;
                }
            }
        }
    }

And I override the GenerateDtoTypes() method and for any type that has the BaseTypeName matching my base class (you could probably just check !IsNullOrEmpty() here as well) I take the code and do a simple string replace on its super(data) call and directly after that I insert the standard code that it inserts for base classes to initialize the properties:

 if (data) {
            for (var property in data) {
                if (data.hasOwnProperty(property))
                    (<any>this)[property] = (<any>data)[property];
            }
        }

Then when I want to generate the actual typescript, I just do this for example:

            var document = await OpenApiDocument.FromUrlAsync("...");
            var settings = new TypeScriptClientGeneratorSettings
            {
                ClassName = "{controller}Client",
            };

            var generator = new MyTypeScriptGenerator(document, settings);

            var code = generator.GenerateFile();

talkbutton avatar Oct 19 '23 17:10 talkbutton