Skip to content

Comments

Fix: enum with nullable property to automatically include null value in validation#240

Merged
daveshanley merged 2 commits intopb33f:mainfrom
k2tzumi:fix/nullable-enum-schema-validation
Feb 14, 2026
Merged

Fix: enum with nullable property to automatically include null value in validation#240
daveshanley merged 2 commits intopb33f:mainfrom
k2tzumi:fix/nullable-enum-schema-validation

Conversation

@k2tzumi
Copy link
Contributor

@k2tzumi k2tzumi commented Feb 14, 2026

Overview

This pull request fixes an issue where OpenAPI 3.0 schemas with both nullable: true and enum properties fail validation when null is returned in the response, even though the schema definition allows null values.

Problem Statement

When validating a response against an OpenAPI 3.0 specification with the following schema definition, a validation error occurred if the field value was null:

{
  "status": {
    "type": "string",
    "enum": [
      "active",
      "inactive",
      "pending",
      "archived"
    ],
    "nullable": true
  }
}

Error Message:

Reason: value must be one of 'active', 'inactive', 'pending', 'archived'
Location: /items/properties/status/enum

Root Cause Analysis

During the investigation phase, we analyzed the OpenAPI 3.0 to JSON Schema conversion process and identified the following:

Investigation Findings

  1. Nullable Transformation Process

    • Located in: helpers/schema_compiler.go - transformOpenAPI30Schema() function
    • The transformNullableSchema() function converts OpenAPI 3.0's nullable: true keyword to JSON Schema compatible format by converting the type field to an array, e.g., "string"["string", "null"]
  2. Missing Enum Handling

    • The transformNullableSchema() function had complete type handling (lines 169-191) including:
      • Single string types: "string"["string", "null"]
      • Array types: ["string", "number"]["string", "number", "null"]
      • AllOf constructs: proper null type handling via oneOf wrapping
    • However, the enum field processing was completely absent
  3. Schema Validation Behavior

    • Analyzed schema_validation/validate_schema.go
    • The JSON Schema validator correctly enforces that values must match one of the enum values
    • Without null in the enum, null values are rejected even though type allows it
  4. Existing Test Coverage Gap

    • Found test case schema_validation/validate_schema_test.go - TestValidateSchema_NullableEnum
    • This test explicitly includes null in the enum definition: enum: [value1, value2, ..., null]
    • No test existed for the case where nullable: true but enum doesn't contain null

Decision Rationale

We decided to automatically add null to the enum values when nullable: true is present and null is not already in the enum for the following reasons:

  1. OpenAPI 3.0 Specification Alignment

    • OpenAPI 3.0 spec recommends including null in enum when using nullable, but doesn't mandate automatic transformation
    • This enhancement improves developer experience by reducing manual burden
  2. Principle of Least Surprise

    • When a developer marks a field as nullable: true, they intuitively expect null to be valid
    • Automatic addition prevents validation errors caused by schema definition inconsistencies
  3. Defensive Duplication Prevention

    • The implementation checks if null already exists in the enum before adding
    • Prevents double-adding null if the enum already contains it
  4. Type vs Enum Consistency

    • The transformation already adds null to the type array
    • Adding null to enum maintains consistency: if type allows null, enum should allow null

Implementation

Changes Made

File: helpers/schema_compiler.go - transformNullableSchema() function

Added enum handling logic after the existing allOf handling:

// Handle enum values - add null if nullable but not already in enum
enum, hasEnum := schema["enum"]
if hasEnum {
	if enumSlice, ok := enum.([]interface{}); ok {
		// Check if null is already in enum
		hasNull := false
		for _, v := range enumSlice {
			if v == nil {
				hasNull = true
				break
			}
		}
		// Add null if not present
		if !hasNull {
			enumSlice = append(enumSlice, nil)
			schema["enum"] = enumSlice
		}
	}
}

Logic Flow:

  1. Check if enum field exists in the schema
  2. If enum is a slice, iterate through values to check for existing null
  3. If null is not found, append nil to the slice
  4. Update the schema's enum field with the modified slice

Test Coverage

File: helpers/schema_compiler_test.go - Added two test cases

  1. TestTransformNullableSchema_EnumWithoutNull

    • Scenario: nullable: true with enum lacking null value
    • Expected: null automatically added to enum
    • Validates: enum length increases, all original values preserved, null present
  2. TestTransformNullableSchema_EnumWithNull

    • Scenario: nullable: true with enum already containing null
    • Expected: null not duplicated
    • Validates: enum length unchanged, exactly one null value exists

Verification

All tests pass:

✅ New enum test cases: PASS
✅ Existing nullable enum test: PASS (backward compatible)
✅ Full test suite: PASS (47 packages)

Backward Compatibility

  • Existing schemas with null explicitly in enum remain unchanged
  • Existing tests continue to pass
  • No breaking changes to public APIs

Edge Cases Handled

  1. Null already in enum: Checked before adding, prevents duplication
  2. Non-slice enum: Type assertion safely handles non-slice enum values
  3. Missing enum field: Conditional check ensures code skips if enum doesn't exist
  4. Type transformation: Enum modification happens after type array transformation, maintaining consistency

Related Issues

Fixes issue where response validation fails for nullable enum fields when null value is returned, applying the improvements outlined in OpenAPI 3.0 nullable handling best practices.

@codecov
Copy link

codecov bot commented Feb 14, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.62%. Comparing base (d8cc0df) to head (e693d65).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #240   +/-   ##
=======================================
  Coverage   97.61%   97.62%           
=======================================
  Files          56       56           
  Lines        5289     5300   +11     
=======================================
+ Hits         5163     5174   +11     
  Misses        126      126           
Flag Coverage Δ
unittests 97.62% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@daveshanley daveshanley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! Which model did this? was it claude or codex?

@daveshanley daveshanley merged commit 2f764db into pb33f:main Feb 14, 2026
4 checks passed
@k2tzumi
Copy link
Contributor Author

k2tzumi commented Feb 14, 2026

Which model did this? was it claude or codex?

The planning, the majority of the code, and the pull request description were generated by Claude Haiku 4.5, while the tests added at the end were generated by Claude Sonnet 4.5. 😃

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.

2 participants