Skip to content

Fix 5331: resolve a null-first type list to its first non-null type - #5428

Open
heath-freenome wants to merge 2 commits into
v7from
fix-5331
Open

heath-freenome wants to merge 2 commits into
v7from
fix-5331

Conversation

@heath-freenome

@heath-freenome heath-freenome commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Reasons for making this change

Fixes #5331

getSchemaType() collapsed a two-entry nullable list to its other type but took type[0] for anything longer, so ['null', 'string', 'number'] resolved to null and, with useFallbackUiForUnsupportedType off, SchemaField rendered NullField: no control to enter a value in. Beside an anyOf/oneOf it rendered an option selector whose every option was blank, since MultiSchemaField hands the parent's type list to an option that names no type of its own.

What changed

@rjsf/utils

  • getSchemaType() resolves a list to its first non-null type that JSON Schema defines, then its first non-null type, then null. ['null', 'foo', 'string'] resolves to string, matching getKnownTypes(). Its return type narrows to string | undefined, since it never returned an array.
  • getSchemaTypeForValue(schema, value) (new, exported): the listed type a value has (a number counts as an integer the list names), otherwise getSchemaType(). It's used by:
    • omitExtraData(), so a string or null held by a ['null', 'object', 'string'] is kept rather than dropped
    • getUiRequiredErrorSchema(), so a string held by that list isn't checked for ui:required properties
    • FallbackField's initial type
  • getWidget() accepts a widget of any non-array type the list names, so a textarea on a ['null', 'number', 'string'] no longer throws. An array widget still needs the list to resolve to array, since only ArrayField hands it a list of items.
  • getOptionalDataControlsType() returns a list of several types whole (getUnionTypes() ?? getSchemaType()), so enableOptionalDataFieldForType: ['object'] doesn't turn the controls on for ['null', 'object', 'string']. This matches the isOptionalDataControlsType() docstring.

@rjsf/core

  • SchemaField renders the field of the listed type that has the field's widget when the resolved type has none by that name. textarea/email on ['null', 'number', 'string'] render StringField and keep '007' as a string. updown on ['string', 'number'] renders NumberField, and checkbox on ['number', 'boolean'] renders BooleanField. Without this the resolved type's field would cast the widget's value to its own type.
  • FallbackField calls getSchemaType() and getSchemaTypeForValue() instead of restating their rules.

Visible side effects

For a null-first list of three or more types:

  • The field renders as the resolved type, and its FieldTemplate gets rjsf-field-<type> instead of rjsf-field-null.
  • getDefaultFormState() fills in that type's defaults. A required ['null', 'boolean', 'string'] starts at false, and a ['null', 'object', 'string'] gets its properties' defaults. A value of another listed type already in the form data, such as a string, is left alone.

omitExtraData() prunes an object value of any list naming object the way it prunes one under a plain { type: 'object' }. This includes lists that don't start with null, such as ['string', 'object']. So a list without properties or additionalProperties drops its object value, as { type: 'object' } already does.

Known gaps, left for follow-ups

  • With the fallback UI on, a null is still checked for ui:required by the resolved type. A field the user hasn't chosen can raise an error they can't see.
  • A widget given by its registered name ('ui:widget': 'TextareaWidget') or as a component doesn't switch SchemaField to another listed type's field.
  • getFieldClassNames()/getDisplayLabel() use the resolved type, not the type of the field SchemaField switched to.

Changelog

CHANGELOG-v7.md:

  • @rjsf/core: one bullet
  • @rjsf/utils: three bullets, one each for getSchemaType(), getSchemaTypeForValue(), and getWidget()/getOptionalDataControlsType()

The v7 upgrade guide and utility-functions.md are updated to match.

Tests

  • getSchemaType(): null-first lists, null in the middle, unknown type names, ['null'], ['null', 'null'], []
  • getSchemaTypeForValue(): listed and unlisted value types, integer for a number, an undefined value
  • getWidget(): a widget of another listed type resolves, array widgets throw unless the list resolves to array, and a widget no listed type has still throws
  • getUiRequiredErrorSchema(), shouldRenderOptionalField() and omitExtraData() cases for the union shapes above. The omitExtraData() cases include a null under ['null', 'object'], which is now kept.
  • SchemaField: both shapes from the issue, plus textarea, email, a uuid format, updown and checkbox on type lists, each asserting the saved value's type

pnpm test passes in all packages (no snapshot changes); lint and knip are clean.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Size limit report

Sizes are minified + brotli, measured by size-limit at 922b555.

A package listed twice is measured both as installed and, on the second row, as its own code with its dependencies excluded. Peer dependencies are always excluded.

Check Base PR Δ
@rjsf/antd 7.14 kB 7.14 kB =
@rjsf/antd (without dependencies) 6.73 kB 6.73 kB =
@rjsf/chakra-ui 18.69 kB 18.69 kB =
@rjsf/chakra-ui (without dependencies) 10.67 kB 10.67 kB =
@rjsf/core 26.84 kB 26.96 kB +0.12 kB
@rjsf/core/markdown 25.53 kB 25.53 kB =
@rjsf/core/testing 59.15 kB 59.24 kB +0.09 kB
@rjsf/core: Form 26.68 kB 26.68 kB =
@rjsf/daisyui 53.57 kB 53.57 kB =
@rjsf/daisyui (without dependencies) 10.74 kB 10.74 kB =
@rjsf/fluentui-rc 5.74 kB 5.74 kB =
@rjsf/mantine 14.29 kB 14.29 kB =
@rjsf/mantine (without dependencies) 9.95 kB 9.95 kB =
@rjsf/mui 6.54 kB 6.54 kB =
@rjsf/react-bootstrap 7.73 kB 7.73 kB =
@rjsf/react-bootstrap (without dependencies) 5.29 kB 5.29 kB =
@rjsf/shadcn 43.40 kB 43.40 kB =
@rjsf/shadcn (without dependencies) 9.10 kB 9.10 kB =
@rjsf/utils 26.61 kB 26.66 kB +0.04 kB
@rjsf/utils (without dependencies) 21.01 kB 21.06 kB +0.05 kB
@rjsf/utils: getUiOptions 0.54 kB 0.54 kB =
@rjsf/validator-ajv8 38.12 kB 38.12 kB =
@rjsf/validator-ajv8 (without dependencies) 2.65 kB 2.65 kB =
@rjsf/validator-ata 86.26 kB 86.26 kB =
@rjsf/validator-ata (without dependencies) 2.42 kB 2.42 kB =
@rjsf/validator-cfworker 7.22 kB 7.22 kB =
@rjsf/validator-cfworker (without dependencies) 2.32 kB 2.32 kB =

Comment thread packages/utils/src/getSchemaType.ts Outdated
// oxlint-disable-next-line prefer-destructuring
type = type[0];
}
type = type.find((t) => t !== 'null') ?? type[0];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Spurious ui:required errors on a non-object value of a null-first list. getUiRequiredErrorSchema() (packages/utils/src/schema/getUiRequiredErrorSchema.ts:186) walks into properties whenever getSchemaType(retrieved) === 'object'. A ['null', 'object', 'string'] used to resolve to null here, so the walk stopped. It now resolves to object, so the walk goes down into the children even when the value is a string or null.

Repro: schema { type: 'object', properties: { v: { type: ['null', 'object', 'string'], properties: { a: { type: 'string' } } } } }, uiSchema { v: { a: { 'ui:required': true } } }, formData { v: 'hello' }. childParentPresent is true and data.a is undefined, so you get must have required property 'a' at v.a and submit is blocked, even though 'hello' is valid.

omitExtraData() got getPruningType() for this exact value-vs-resolved-type mismatch, but this sibling consumer didn't. It should resolve the type from the value it walks too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 922b555. getUiRequiredErrorSchema() now gets the type from the value through the new getSchemaTypeForValue(), so a string under ['null', 'object', 'string'] isn't walked into. Your repro is a test in getUiRequiredErrorSchema.test.ts. A null is still read by the resolved type, because ObjectField renders an object field's properties even when it holds null.

Comment thread CHANGELOG-v7.md Outdated
- `logOnce()` compares a plain-object or array `error` by its JSON, so two different plain-object payloads logged with the same message are no longer taken for one, and the `logUnsupportedDefaultForEnum()` error spells an object default out as JSON rather than `[object Object]`. `enumOptionValueLabel()` spells a value the same way, so a value reads the same in an option label as in a warning: a `Date`, a `Map` or a nested `BigInt` is spelled as `String()` spells it, and a circular plain object or array no longer throws. ([#5382](https://github.com/rjsf-team/react-jsonschema-form/pull/5382))
- **BREAKING CHANGE:** `UiSchemaDefinitions<S, F>` drops its `T` parameter and types every entry as `UiSchema<unknown, S, F>`, the type `UiSchema`'s `ui:definitions` key now uses too. A definition applies to whichever field references it, so typing it by the root form's data rejected the nested keys of the field it describes ([#5372](https://github.com/rjsf-team/react-jsonschema-form/pull/5372))
- Fixed `getDateTimeLocalValue()` returning `undefined` for an epoch number or a `Date`: a finite number or a valid `Date` is now converted to its UTC ISO string for `date-time`/`datetime`, to the day it names for a `date` that is a UTC midnight, and to local wall-clock time for any other format. Its result also gains `requiresOffset`, true for `date-time`/`datetime` ([#5393](https://github.com/rjsf-team/react-jsonschema-form/issues/5393))
- Fixed `getSchemaType()` returning `null` for a `type` list of three or more entries that starts with `null`: it returns the first type in the list other than `null`, as it already did for a two-entry list, and `null` only for a list naming nothing else ([#5331](https://github.com/rjsf-team/react-jsonschema-form/issues/5331))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This also changes getDefaultFormState() for any null-first list, and neither changelog entry says so. getDefaultBasedOnSchemaType() switches on getSchemaType() (getDefaultFormState.ts:892). So for a required ['null', 'boolean', 'string'] with no default, the form now starts at false (the case 'boolean' branch), where before it was left unset. A null-first list naming object or array now gets object/array defaults computed for it.

The upgrade guide mentions this, but these CHANGELOG-v7.md bullets and the PR body only cover rendering and object/array defaults. A form that relied on the field starting empty now gets a value it never set.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added in 922b555. The getSchemaType() bullet now says getDefaultFormState() follows it, so a null-first list gets that type's defaults. The PR description gives both examples: false for a required ['null', 'boolean', 'string'] and object defaults for ['null', 'object', 'string'].

Comment thread packages/utils/src/getSchemaType.ts Outdated
// oxlint-disable-next-line prefer-destructuring
type = type[0];
}
type = type.find((t) => t !== 'null') ?? type[0];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Optional Data Controls now turn on for a union field. getOptionalDataControlsType() (shouldRenderOptionalField.ts:46) returns getSchemaType(schema). A ['null', 'object', 'string'] (or ['null', 'array', ...]) now answers object/array instead of null. So enableOptionalDataFieldForType: ['object'] now enables the optional-data controls for a field that allows several types, which contradicts the isOptionalDataControlsType() docstring ("A field whose type is a list of several types is never one of them"). getUiRequiredErrorSchema() uses the same predicate to gate its walk.

This was already true for lists that don't start with null (['object', 'string']), but this change brings null-first lists into it. Either check getUnionTypes() there, or update the docstring.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 922b555 with getUnionTypes(). getOptionalDataControlsType() now returns getUnionTypes(schema) ?? getSchemaType(schema), so a list of several non-null types comes back whole and isOptionalDataControlsType() rejects it, as its docstring says. That covers ['object', 'string'] too. getSchemaTypesForXxxOf() does the same for each option.

Comment thread packages/utils/src/getSchemaType.ts Outdated
// oxlint-disable-next-line prefer-destructuring
type = type[0];
}
type = type.find((t) => t !== 'null') ?? type[0];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reuse: this expression is exactly getDefaultType() in packages/core/src/components/fields/FallbackField.tsx:53 (types.find((aType) => aType !== 'null') ?? types[0]). The PR body even calls it "the rule FallbackField already uses". Pulling it into one small util that both call keeps the "first non-null type" rule from drifting between the two.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 922b555. FallbackField's getDefaultType() is now getSchemaType({ type: types }). The rule also gained the known-type filter from your later comment, so the two can't drift.

Comment thread packages/utils/src/getSchemaType.ts Outdated
// oxlint-disable-next-line prefer-destructuring
type = type[0];
}
type = type.find((t) => t !== 'null') ?? type[0];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Simplification: after this line, getSchemaType() can't return an array (that was also true before, since the old branches only returned type[0] or find). Its declared return type is still string | string[] | undefined, though, which keeps dead code alive in callers: Array.isArray(schemaType) ? schemaType[0] : ... in SchemaField.getFieldComponent() (SchemaField.tsx:222, a function this PR touches) and the .flat() in getSchemaTypesForXxxOf(). Narrowing the return type to string | undefined lets both go.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 922b555. The return type is string | undefined. The Array.isArray(schemaType) branch in SchemaField and the .flat() in getSchemaTypesForXxxOf() are gone.

* @param value - The form data being pruned
* @returns - The type to prune `value` as
*/
function getPruningType<S extends StrictRJSFSchema>(schema: S, value: unknown) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Altitude: value-aware type resolution lives only in omitExtraData. Now that more lists resolve to a container type, other consumers branch on getSchemaType() while handed a value of another listed type. getUiRequiredErrorSchema() (walks object children of a string value) and getDefaultBasedOnSchemaType() (computes object defaults over a string rawFormData) both do this. Consider moving getPruningType() into @rjsf/utils as a shared helper, e.g. getSchemaTypeForValue(schema, value), and using it at those call sites. Otherwise each consumer has to rediscover this mismatch on its own.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 922b555. getPruningType() became getSchemaTypeForValue(schema, value) in @rjsf/utils. It's used by omitExtraData(), getUiRequiredErrorSchema() and FallbackField's initial type. I didn't change getDefaultBasedOnSchemaType(): computeDefaults() already checks isObject(rawFormData), so { v: 'hello' } under ['null', 'object', 'string'] with a property default stays { v: 'hello' }. I confirmed that at the new head.

*/
function getPruningType<S extends StrictRJSFSchema>(schema: S, value: unknown) {
const { type } = schema;
if (Array.isArray(type) && value !== undefined) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Dead guard: value !== undefined can never be false here. The only caller is omit(), which returns at the top when source === undefined, before it reaches getPruningType(localSchema, source). Drop the check, or say in the docblock why it's needed if it's meant to protect future callers. As written, it suggests guessType(undefined) === 'null' is a live case.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

getPruningType() is gone. Its replacement, getSchemaTypeForValue(), is a public util with other callers, so the undefined guard stays there. The docblock now says why: guessType(undefined) reports null, and an undefined value means there's no value, not a null one. A test covers it.

Comment thread packages/utils/src/getSchemaType.ts Outdated
// oxlint-disable-next-line prefer-destructuring
type = type[0];
}
type = type.find((t) => t !== 'null') ?? type[0];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A null-first list with a string-only ui:widget now crashes the whole form.

{ type: ['null', 'number', 'string'] } with uiSchema: { 'ui:widget': 'textarea' } used to resolve to null and render NullField. It now resolves to number, so NumberField → StringField → getWidget() looks textarea up in the number widget map and throws No widget 'textarea' for type 'number' in schema: {"type":["null","number","string"]}. I reproduced this with createFormComponent at 3a8a5f9. The render throws, so the whole Form goes down, not just that field. Any string-only alias (textarea, email, password, ...) on a ['null','number'|'integer'|'boolean', 'string', ...] list does the same, as does a number-only alias (updown, range) on ['null','string','number'].

['number', 'string'] already behaved this way, but this PR exposes null-first lists that rendered before. At minimum it needs a changelog/migration note. Better: when the named alias isn't in the resolved type's map, getWidget() could fall back to another type in the list that does have it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 922b555, in two places:

  • getWidget() accepts a widget of any non-array type the list names. An array widget still throws unless the list resolves to array, because only ArrayField hands it a list of items.
  • SchemaField renders the field of the listed type that has the widget when the resolved type has none by that name. Without this, NumberField would render the textarea and save '007' as 7. textarea/email on ['null', 'number', 'string'] now render StringField, updown on ['null', 'string', 'number'] renders NumberField, and checkbox on ['number', 'boolean'] renders BooleanField. Each has a test.

A widget given by its registered name ('TextareaWidget') or as a component doesn't trigger the switch yet. That's listed as a known gap in the description.


// A select holds one of its constants as a whole, so an `object` or `array` one has no contents to prune
const type = isWholeValueSelect<S>(localSchema) ? undefined : getSchemaType<S>(localSchema);
const type = isWholeValueSelect<S>(localSchema) ? undefined : getPruningType<S>(localSchema, source);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

omitExtraData() now drops the whole object value of any type list without properties, including lists that have no null at all.

getPruningType() prunes an object value as object whenever the list includes object, and handleObject() treats a missing additionalProperties as false. So a properties-less object comes out {}, and setProperty() then drops the key. I checked base and PR head with the same input:

schema of v formData base (9af3757) this PR
{ type: ['string', 'object'] } { v: { k: 1 } } { v: { k: 1 } } {}
{ type: ['null', 'object', 'string'] } { v: { k: 1 } } { v: { k: 1 } } {}

With omitExtraData/liveOmit on, an object that came in through formData is now silently removed on submit. The first row isn't null-first at all, so the change reaches well past #5331. The changelog bullet only describes what is now kept. Either limit the value-type override to keeping non-container values (and let objects/arrays prune as before), or call out the new pruning of ['…', 'object'] values in the changelog.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I kept this behavior on purpose. An object value of a list naming object is now pruned exactly like one under a plain { type: 'object' }. I checked at the new head: { v: { k: 1 } } gives {} for { type: 'object' }, ['string', 'object'] and ['null', 'object', 'string'] alike. A properties-less object schema means no properties are allowed, and omitExtraData already takes it that way for a single type. The old result for the union came from resolving to string and skipping object pruning altogether. I've called it out under side effects in the PR description, ['string', 'object'] included. I can also add it to the changelog if you'd like it there.

Comment thread packages/utils/src/getSchemaType.ts Outdated
// oxlint-disable-next-line prefer-destructuring
type = type[0];
}
type = type.find((t) => t !== 'null') ?? type[0];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

"First non-null" picks an unrecognized type name over a renderable one.

For ['null', 'foo', 'string'], find((t) => t !== 'null') returns foo. COMPONENT_TYPES has no foo, so SchemaField falls to FallbackField, and with the fallback UI off that renders the unsupported-field box. string is right there in the list, and getKnownTypes()/getUnionTypes() already filter to JSON_SCHEMA_TYPES. Filtering to known, non-null types first (type.find((t) => t !== 'null' && JSON_SCHEMA_TYPES.includes(t)) ?? type[0]) would make the resolution agree with getKnownTypes().

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 922b555. getSchemaType() now picks the first non-null type in JSON_SCHEMA_TYPES, then the first non-null type, then type[0]. So ['null', 'foo', 'string'] resolves to string and ['null', 'foo'] still resolves to foo. Both are test cases.

Comment thread CHANGELOG-v7.md Outdated
- `logOnce()` compares a plain-object or array `error` by its JSON, so two different plain-object payloads logged with the same message are no longer taken for one, and the `logUnsupportedDefaultForEnum()` error spells an object default out as JSON rather than `[object Object]`. `enumOptionValueLabel()` spells a value the same way, so a value reads the same in an option label as in a warning: a `Date`, a `Map` or a nested `BigInt` is spelled as `String()` spells it, and a circular plain object or array no longer throws. ([#5382](https://github.com/rjsf-team/react-jsonschema-form/pull/5382))
- **BREAKING CHANGE:** `UiSchemaDefinitions<S, F>` drops its `T` parameter and types every entry as `UiSchema<unknown, S, F>`, the type `UiSchema`'s `ui:definitions` key now uses too. A definition applies to whichever field references it, so typing it by the root form's data rejected the nested keys of the field it describes ([#5372](https://github.com/rjsf-team/react-jsonschema-form/pull/5372))
- Fixed `getDateTimeLocalValue()` returning `undefined` for an epoch number or a `Date`: a finite number or a valid `Date` is now converted to its UTC ISO string for `date-time`/`datetime`, to the day it names for a `date` that is a UTC midnight, and to local wall-clock time for any other format. Its result also gains `requiresOffset`, true for `date-time`/`datetime` ([#5393](https://github.com/rjsf-team/react-jsonschema-form/issues/5393))
- Fixed `getSchemaType()` returning `null` for a `type` list of three or more entries that starts with `null`: it returns the first type in the list other than `null`, as it already did for a two-entry list, and `null` only for a list naming nothing else ([#5331](https://github.com/rjsf-team/react-jsonschema-form/issues/5331))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The new default-filling for null-first lists isn't in the changelog.

Because these lists now resolve to object/array, getDefaultBasedOnSchemaType() fills them in on load. An optional v: { type: ['null', 'object', 'string'], properties: { a: { type: 'string', default: 'x' } } } with formData {} gave {} on base and gives { v: { a: 'x' } } here (checked at both commits). That value is then submitted even if the user never touched the field. The PR description and the v7 upgrade guide mention it, but this getSchemaType() bullet and the @rjsf/core bullet don't. A user scanning CHANGELOG-v7.md would miss a change to initial form data.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added in 922b555. The getSchemaType() bullet now says getDefaultFormState() follows it, and the PR description has your { v: { a: 'x' } } example.

* @param value - The form data being pruned
* @returns - The type to prune `value` as
*/
function getPruningType<S extends StrictRJSFSchema>(schema: S, value: unknown) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reuse: this re-implements getInitialType() from FallbackField.

packages/core/src/components/fields/FallbackField.tsx getInitialType() already does guessType(formData) → types.includes(dataType) → dataType === 'number' && types.includes('integer'). That's the same matching, so the "which listed type does this value have" rule now lives in two packages that can drift apart. You could move one helper into @rjsf/utils (e.g. getListedTypeOfValue(types, value)), and have FallbackField and omitExtraData() both call it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 922b555 as getSchemaTypeForValue(schema, value), which takes the schema instead of a bare list so it can fall back to getSchemaType(). FallbackField's getInitialType() and omitExtraData() both call it now.

Comment thread packages/utils/src/getSchemaType.ts Outdated
// oxlint-disable-next-line prefer-destructuring
type = type[0];
}
type = type.find((t) => t !== 'null') ?? type[0];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reuse: this expression is now FallbackField's getDefaultType() word for word.

packages/core/src/components/fields/FallbackField.tsx:52-54 is types.find((aType) => aType !== 'null') ?? types[0]. Now that getSchemaType() encodes the same rule, getDefaultType() could go and its call sites could use getSchemaType(). Otherwise the first-non-null rule is stated twice and can drift.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done in 922b555: getDefaultType() now just calls getSchemaType().

*/
function getPruningType<S extends StrictRJSFSchema>(schema: S, value: unknown) {
const { type } = schema;
if (Array.isArray(type) && value !== undefined) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Dead guard: value !== undefined can never be false here. The only caller is omit(), which returns early on source === undefined (the first line of omit()) before it gets here. if (Array.isArray(type)) is enough.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

See the reply above: the guard moved into the public getSchemaTypeForValue(), where an undefined value can reach it, and its docblock says why.

Comment thread packages/utils/src/getInputProps.ts Outdated
// type resolves to its first entry, which says nothing about what the others accept, so it keeps the plain text
// input it had before
// type resolves to its first non-null entry, which says nothing about what the others accept, so it keeps the plain
// text input it had before

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

CLAUDE.md: "Don't leave 'before/after' or diff-style commentary in code comments … That belongs in commit messages and PR descriptions, not in code that has to keep reading true after the change ships." (CLAUDE.md → Code comments)

This rewritten comment keeps "so it keeps the plain text input it had before". Suggest saying what the code does now, e.g. "…says nothing about what the others accept, so such a union gets a plain text input".

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Reworded in 922b555: "…resolves to just one of them, which says nothing about what the others accept, so such a union gets a plain text input".

`getSchemaType()` collapsed a two-entry nullable list to its other type but took
`type[0]` for anything longer, so `['null', 'string', 'number']` resolved to
`null` and `SchemaField` rendered `NullField`: no control to enter a value in,
and beside an `anyOf`/`oneOf` an option selector whose every option rendered
nothing, since `MultiSchemaField` hands the parent's type list to each option.

- `getSchemaType()` now returns the first type other than `null` for any list,
  and `null` only for a list naming nothing else, matching what `FallbackField`
  already picks as its initial type
- `omitExtraData()` prunes a value of any type a `type` list allows as that type
  rather than by the one type `getSchemaType()` resolves the list to, so a
  `['null', 'object', 'string']`, which now resolves to `object`, no longer drops
  a valid `null` or string, and a `['null', 'object']` no longer drops a `null`
- Add `getSchemaType()` cases and `SchemaField` regression tests for the plain
  and `oneOf` shapes from the issue
- Point the `getWidget()` null-type test at a plain `null` select, since a
  null-first list no longer reaches the null widget map
- Update the utility-functions docs, the v7 upgrade guide's `getSchemaType()`
  and `getWidget()` bullets, and the `SchemaField` and `getInputProps()`
  comments that described the old resolution

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	CHANGELOG-v7.md

# Conflicts:
#	CHANGELOG-v7.md
- Add getSchemaTypeForValue() and use it in omitExtraData(),
  getUiRequiredErrorSchema() and FallbackField, so a value of another
  listed type is read as that type
- getSchemaType() prefers a type JSON Schema defines and returns
  string | undefined
- getWidget() accepts a widget of any non-array type a type list names;
  SchemaField renders the field of the listed type that has the widget,
  so its value isn't cast to the resolved type
- getOptionalDataControlsType() returns a list of several types whole
- FallbackField reuses the utils rules instead of restating them
- Update docs and condense the changelog entries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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