Skip to content

Honor OpenAPI required + nullable for non-nullable models (TypeScript, Python & Java, opt-in)#7795

Open
kev-flex wants to merge 9 commits into
microsoft:mainfrom
kev-flex:feature/required-properties-non-nullable
Open

Honor OpenAPI required + nullable for non-nullable models (TypeScript, Python & Java, opt-in)#7795
kev-flex wants to merge 9 commits into
microsoft:mainfrom
kev-flex:feature/required-properties-non-nullable

Conversation

@kev-flex

@kev-flex kev-flex commented Jun 15, 2026

Copy link
Copy Markdown

Summary

This adds an opt-in generation flag, --make-required-properties-non-nullable (alias --mrpnn), that makes Kiota honor the OpenAPI required array alongside the nullable / type: [..., "null"] markers. With it on, a property that is required and not explicitly nullable is generated as non-optional and non-nullable, so consumers don't need to write ?? "", !, or assert x is not None for fields the server always sends.

Refs #3911. It builds on the groundwork from #7585 and turns the optimization on for TypeScript, Python, and Java.

The flag defaults to false, so existing output stays byte-for-byte identical for every language unless someone opts in. That keeps current clients safe and lets us enable languages one at a time.

How it works

The pipeline builds a single language-agnostic CodeDOM and each writer renders it. Most writers already derive nullability from Type.IsNullable, so the change comes down to one signal:

  • CodeProperty.IsRequired is populated from the parent schema's required array, regardless of the flag.
  • OpenApiSchemaExtensions.IsExplicitlyNullable() detects OAS 3.0 nullable: true and OAS 3.1 type: [..., "null"] / anyOf: [{ type: null }].
  • When the flag is on, KiotaBuilder sets Type.IsNullable = false for required, non-explicitly-nullable, non-collection custom properties. Collections are left alone, since their IsNullable also drives element nullability and the serializer API.

The flip lives in the builder but is gated to the languages whose writers handle it and that are covered by tests here: TypeScript, Python, and Java. Every other language keeps its current nullable output, so turning the flag on is a no-op for them until support lands.

Per language:

  • TypeScript (CodePropertyWriter): the interface writer used to hardcode ?: T | null. It now drops the ? for required properties and drops | null unless the schema is explicitly nullable. The flag is threaded through TypeScriptConventionService, because otherwise a required-but-nullable property looks identical in the CodeDOM to a flag-off required property.
  • Python: no writer change. The dataclass field writer already keys Optional[...] off IsNullable. Required primitives get a type-correct zero default ('', 0, 0.0, False) so the field stays sound; required objects and enums have no zero literal, so they keep a None default behind the non-optional type. A default is needed either way for the no-arg construction in create_from_discriminator_value() and for valid dataclass field ordering.
  • Java: no writer change. The property writer already emits @jakarta.annotation.Nonnull instead of @Nullable when IsNullable is false, and no Java refiner re-nullifies model properties.

On the CLI side the flag is available on kiota generate, kiota client add, and kiota client edit. Edit uses a nullable bool? so it won't overwrite a previously persisted value. The setting is saved to workspace.json through ApiClientConfiguration and read back into GenerationConfiguration, the same way ExcludeBackwardCompatible is wired, and it emits a telemetry tag like the other generation toggles.

Generated output (flag on)

TypeScript:

amount: number;             // required, non-nullable
description: string | null; // required, but schema is nullable: drop ?, keep | null
note?: string | null;       // optional: unchanged
tags: string[] | null;      // required collection: drop ?, keep nullability

Python:

amount: int = 0                   # required primitive: non-optional, type-correct zero default
owner: Owner = None               # required object/enum: non-optional, None default
description: Optional[str] = None  # required, but schema is nullable
tags: Optional[list[str]] = None   # collection: unchanged

For Python, required primitives use a real zero-value default ('', 0, 0.0, False), so the field is type-correct. Required objects and enums have no zero-value literal, so they render non-optional with a None default. That None is an advisory non-null that the deserializer always overwrites, which is the same compile-time-only guarantee Java's @Nonnull and TypeScript's erased types already give.

Java:

@jakarta.annotation.Nonnull   // required, non-nullable
public Integer amount;
@jakarta.annotation.Nullable  // required, but schema is nullable
public String description;

Configuration and docs

  • CHANGELOG.md: entry added under the Added heading of [Unreleased].
  • specs/cli/client-add.md and specs/cli/client-edit.md: the new option is listed in the parameter tables. The generate command reads it from workspace.json, so its table is unchanged.
  • specs/schemas/workspace.json: makeRequiredPropertiesNonNullable added to the client schema so authored or edited workspaces validate.
  • Telemetry: KiotaGenerateCommandHandler emits command.params.make_required_properties_non_nullable, matching the existing per-flag tags.
  • CLI help: the option description covers the behavior, the default, and which languages are currently supported.

Scope and limitations

These are intentional, and called out so reviewers don't have to guess.

  • Support is per-language and incremental. The flip happens in the language-agnostic builder but is gated to the three writers validated here. Every other language keeps its current nullable output, which the tests verify is a byte-for-byte no-op through full refinement (including the PHP and Go MakeModelPropertiesNullable pass). New languages can be added later.
  • The non-null guarantee is optimistic. As with the types Kiota already emits, it is compile-time only: TypeScript types are erased, Java's @Nonnull is an annotation, and Python annotations aren't enforced. A non-conformant server that drops a required field will leave undefined / null / None behind a non-null type. No runtime check is added.
  • Required declared inside an allOf member is not tightened. Required-ness is read from a schema's own (allOf-merged) required array, and MergeIntersectionSchemaEntries merges member properties but not their required arrays. So a property marked required only inside an allOf member is treated as not required and stays nullable. This never marks an omittable field as non-null, and it matches the existing required handling.

Testing

  • The existing Kiota.Builder.Tests and Kiota.Tests suites pass unchanged, which confirms the default-off no-op.
  • Builder level (KiotaBuilderTests, the Issue-3911 region): the full on/off matrix at the CodeDOM level across scalar, integer, enum, object ref, collection, required-nullable, and optional, plus OAS 3.1 cases where type: [string, "null"] stays nullable and a 3.1 required scalar is made non-nullable.
  • Per-language behavior with the flag on, run through the full build and language-refinement pipeline. For the supported languages (Python, Java) a required non-nullable property stays non-nullable through refinement; TypeScript is checked at the writer level because it lowers models to interfaces during refinement. For the rest (C#, Go, PHP, Dart) a required property stays nullable after refinement, which proves the flag is a no-op for them.
  • Writers: TypeScript interface and serialization-function rendering (CodePropertyWriterTests, CodeFunctionWriterTests), Python dataclass field rendering (CodeMethodWriterTests), and Java @Nonnull rendering (CodePropertyWriterTests).
  • OpenApiSchemaExtensionsTests: IsExplicitlyNullable() across OAS 3.0 nullable: true, the OAS 3.1 type null union, anyOf / oneOf null members, and the non-nullable and null-receiver cases.
  • Workspace round-trip (ApiClientConfigurationTests): the flag is asserted through all three persistence paths (config to client config, the update path, and Clone()).

@kev-flex kev-flex requested a review from a team as a code owner June 15, 2026 21:25
@kev-flex kev-flex marked this pull request as draft June 15, 2026 21:27
@kev-flex kev-flex force-pushed the feature/required-properties-non-nullable branch from 3906ac9 to 128295e Compare June 15, 2026 21:29
@kev-flex

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

kev-flex and others added 3 commits June 15, 2026 15:46
…neOf nullability

Addresses issues found in review of the MakeRequiredPropertiesNonNullable flag:

- TypeScript: a required reference to a scalar alias (e.g. a oneOf of
  string-enums that collapses to `type X = string`) could flip the
  discriminator factory's composed ReturnType to non-nullable, emitting
  `: X` over a `parseNode?.get*Value()` body (`X | undefined`) -> tsc error.
  CodeFunctionWriter now forces primitive-composed factory return types
  nullable; no-op when the flag is off. Covered by a new regression test.

- Workspace flow: persist MakeRequiredPropertiesNonNullable in
  ApiClientConfiguration (constructor, update, clone) and expose it on
  `kiota client add` / `kiota client edit` so the flag round-trips through
  kiota-workspace.json instead of silently reverting on regeneration.

- OpenApiSchemaExtensions.IsExplicitlyNullable now also recognizes the OAS
  3.1 `oneOf: [{ type: null }, ...]` nullability pattern (previously only
  anyOf), so such required properties are correctly left nullable. Covered
  by a new unit test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@kev-flex kev-flex marked this pull request as ready for review June 19, 2026 00:22
@kev-flex kev-flex marked this pull request as draft June 19, 2026 00:22
@kev-flex kev-flex changed the title Honor OpenAPI required + nullable for non-nullable models (TypeScript & Python, opt-in) Honor OpenAPI required + nullable for non-nullable models (TypeScript, Python & Java, opt-in) Jun 19, 2026
@kev-flex kev-flex marked this pull request as ready for review June 19, 2026 02:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant