diff --git a/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts index 0ebf674ee..0bea31f38 100644 --- a/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/CSharpRenderer.ts @@ -206,7 +206,11 @@ export class CSharpRenderer extends ConvenienceRenderer { } const csType = this.csType(t, follow, withIssues); - if (isValueType(t) || this._csOptions.version >= 8) { + if ( + isValueType(t) || + this._csOptions.version >= 8 || + this._csOptions.nullableReferenceTypes + ) { return [csType, "?"]; } else { return csType; diff --git a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts index ce0cf35ee..1bff0ea18 100644 --- a/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts +++ b/packages/quicktype-core/src/language/CSharp/NewtonSoftCSharpRenderer.ts @@ -202,7 +202,12 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { } protected emitDefaultFollowingComments(): void { - if (!this._needHelpers || this._options.version < 8) return; + if ( + !this._needHelpers || + (this._options.version < 8 && !this._options.nullableReferenceTypes) + ) { + return; + } this.emitLine("#pragma warning restore CS8618"); this.emitLine("#pragma warning restore CS8601"); @@ -247,7 +252,10 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { ); }); - if (this._options.version >= 8) { + if ( + this._options.version >= 8 || + this._options.nullableReferenceTypes + ) { this.emitLine("#nullable enable"); this.emitLine("#pragma warning disable CS8618"); this.emitLine("#pragma warning disable CS8601"); @@ -305,7 +313,10 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { ? [", NullValueHandling = ", nullValueHandlingClass, ".Ignore"] : []; let required: Sourcelike; - if (!this._options.checkRequired || (isOptional && isNullable)) { + if ( + !this._options.checkRequired || + (isOptional && (isNullable || this._options.nullableReferenceTypes)) + ) { required = [nullValueHandling]; } else if (isOptional && !isNullable) { required = [ @@ -374,6 +385,9 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { } const csType = this.topLevelResultType(t); + const fromJsonType = this._options.nullableReferenceTypes + ? this.nullableCSType(t) + : csType; this.emitType( undefined, AccessModifier.Public, @@ -383,7 +397,7 @@ export class NewtonsoftCSharpRenderer extends CSharpRenderer { () => { // FIXME: Make FromJson a Named this.emitExpressionMember( - ["public static ", csType, " FromJson(string json)"], + ["public static ", fromJsonType, " FromJson(string json)"], [ "JsonConvert.DeserializeObject<", csType, diff --git a/packages/quicktype-core/src/language/CSharp/language.ts b/packages/quicktype-core/src/language/CSharp/language.ts index 81bb0295f..997fbdca6 100644 --- a/packages/quicktype-core/src/language/CSharp/language.ts +++ b/packages/quicktype-core/src/language/CSharp/language.ts @@ -74,6 +74,12 @@ export const cSharpOptions = { "8", "secondary", ), + nullableReferenceTypes: new BooleanOption( + "nullable-reference-types", + "Use nullable reference types for optional properties (C# 8+)", + false, + "secondary", + ), virtual: new BooleanOption("virtual", "Generate virtual properties", false), useRecords: new BooleanOption( "use-records", diff --git a/test/languages.ts b/test/languages.ts index 5e721fad1..48b0dcda4 100644 --- a/test/languages.ts +++ b/test/languages.ts @@ -165,6 +165,7 @@ export const CSharpLanguage: Language = { // language-version code paths covered. { "csharp-version": "5" }, { "csharp-version": "6" }, + { "csharp-version": "6", "nullable-reference-types": true }, { density: "dense" }, { "number-type": "decimal" }, { "any-type": "dynamic" }, diff --git a/test/unit/csharp-nullable-reference-types.test.ts b/test/unit/csharp-nullable-reference-types.test.ts new file mode 100644 index 000000000..59b6ca0ca --- /dev/null +++ b/test/unit/csharp-nullable-reference-types.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, test } from "vitest"; + +import { + InputData, + JSONSchemaInput, + type RendererOptions, + quicktype, +} from "../../packages/quicktype-core/src/index.js"; + +async function renderCSharp(rendererOptions: RendererOptions): Promise { + const schemaInput = new JSONSchemaInput(undefined); + await schemaInput.addSource({ + name: "Request", + schema: JSON.stringify({ + type: "object", + properties: { + profile: { + type: "object", + properties: { id: { type: "string" } }, + }, + displayName: { type: "string" }, + }, + }), + }); + + const inputData = new InputData(); + inputData.addInput(schemaInput); + const result = await quicktype({ + inputData, + lang: "csharp", + rendererOptions, + }); + return result.lines.join("\n"); +} + +describe("C# nullable reference types", () => { + test("keeps the default Newtonsoft output unchanged", async () => { + const output = await renderCSharp({ + framework: "NewtonSoft", + "csharp-version": "6", + "check-required": true, + }); + + expect(output).toContain( + '[JsonProperty("profile", Required = Required.DisallowNull, NullValueHandling = NullValueHandling.Ignore)]', + ); + expect(output).toContain("public Profile Profile { get; set; }"); + expect(output).toContain("public static Request FromJson(string json)"); + expect(output).not.toContain("#nullable enable"); + }); + + test("opts into nullable optional properties for Newtonsoft", async () => { + const output = await renderCSharp({ + framework: "NewtonSoft", + "csharp-version": "6", + "check-required": true, + "nullable-reference-types": true, + }); + + expect(output).toContain("#nullable enable"); + expect(output).toContain( + '[JsonProperty("profile", NullValueHandling = NullValueHandling.Ignore)]', + ); + expect(output).not.toContain("Required.DisallowNull"); + expect(output).toContain("public Profile? Profile { get; set; }"); + expect(output).toContain("public string? DisplayName { get; set; }"); + expect(output).toContain( + "public static Request? FromJson(string json)", + ); + }); +}); diff --git a/test/unit/renderer-options.test-d.ts b/test/unit/renderer-options.test-d.ts index 33fd01036..717a7bd07 100644 --- a/test/unit/renderer-options.test-d.ts +++ b/test/unit/renderer-options.test-d.ts @@ -23,6 +23,7 @@ describe("rendererOptions typing", () => { namespace: "Acme", framework: "SystemTextJson", "csharp-version": "6", + "nullable-reference-types": true, }, }); });