rjsf-team / react-jsonschema-form

A React component for building Web forms from JSON Schema.
https://rjsf-team.github.io/react-jsonschema-form/
Apache License 2.0
14.1k stars 2.18k forks source link

Application freeze #4262

Open jakubkanna opened 1 month ago

jakubkanna commented 1 month ago

Prerequisites

What theme are you using?

core

Version

5.x

Current Behavior

Using json schema generated from Prisma database causing application to freeze. (It has many nested references).

Expected Behavior

It should render form.

Steps To Reproduce

  1. Generate schema via prisma-json-schema-generator
  2. Copy paste schema into playground

Environment

Playground

Anything else?

Schema ```json { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "GeneralSection": { "type": "object", "properties": { "id": { "type": "integer" }, "title": { "type": "string" }, "description": { "type": ["string", "null"] }, "published": { "type": "boolean", "default": false }, "slug": { "type": "string" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/Tag" } }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": ["string", "null"], "format": "date-time" }, "Project": { "type": "array", "items": { "$ref": "#/definitions/Project" } }, "Work": { "type": "array", "items": { "$ref": "#/definitions/Work" } }, "Post": { "type": "array", "items": { "$ref": "#/definitions/Post" } } } }, "ImageRef": { "type": "object", "properties": { "etag": { "type": "string" }, "public_id": { "type": "string" }, "type": { "type": "string", "default": "IMAGE", "enum": ["IMAGE", "VIDEO", "THREE_D"] }, "cld_url": { "type": "string" }, "path": { "type": "string" }, "filename": { "type": "string" }, "format": { "type": "string" }, "bytes": { "type": "integer" }, "description": { "type": "string", "default": "" }, "width": { "type": "integer" }, "height": { "type": "integer" }, "tags": { "type": "array", "items": { "$ref": "#/definitions/Tag" } }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": ["string", "null"], "format": "date-time" }, "Preferences": { "anyOf": [ { "$ref": "#/definitions/Preferences" }, { "type": "null" } ] }, "preferencesId": { "type": ["integer", "null"] }, "Work": { "type": "array", "items": { "$ref": "#/definitions/Work" } }, "Project": { "type": "array", "items": { "$ref": "#/definitions/Project" } } } }, "VideoRef": { "type": "object", "properties": { "etag": { "type": "string" }, "type": { "type": "string", "default": "VIDEO", "enum": ["IMAGE", "VIDEO", "THREE_D"] }, "id": { "type": ["string", "null"] }, "vimeo_url": { "type": ["string", "null"] }, "sc_url": { "type": ["string", "null"] }, "yt_url": { "type": ["string", "null"] }, "title": { "type": "string", "default": "Untitled" }, "duration": { "type": ["string", "null"] }, "definition": { "type": ["string", "null"] }, "description": { "type": ["string", "null"] }, "thumbnail": { "type": ["string", "null"] }, "tags": { "type": "array", "items": { "$ref": "#/definitions/Tag" } }, "player_loop": { "type": "boolean", "default": true }, "player_muted": { "type": "boolean", "default": false }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": ["string", "null"], "format": "date-time" }, "Preferences": { "anyOf": [ { "$ref": "#/definitions/Preferences" }, { "type": "null" } ] }, "preferencesId": { "type": ["integer", "null"] }, "Work": { "type": "array", "items": { "$ref": "#/definitions/Work" } }, "Project": { "type": "array", "items": { "$ref": "#/definitions/Project" } } } }, "Post": { "type": "object", "properties": { "id": { "type": "integer" }, "html": { "type": ["string", "null"] }, "general": { "$ref": "#/definitions/GeneralSection" }, "generalId": { "type": "integer" }, "author": { "$ref": "#/definitions/User" }, "authorId": { "type": "integer" } } }, "Preferences": { "type": "object", "properties": { "id": { "type": "integer" }, "creator_name": { "type": "string", "default": "Creator Name" }, "homepage_heading": { "type": "string", "default": "Homepage" }, "homepage_subheading": { "type": "string", "default": "Sub-Heading" }, "homepage_background_image": { "anyOf": [ { "$ref": "#/definitions/ImageRef" }, { "type": "null" } ] }, "homepage_background_video": { "anyOf": [ { "$ref": "#/definitions/VideoRef" }, { "type": "null" } ] }, "enable_dashboard_darkmode": { "type": "boolean", "default": false }, "enable_portfolio_pdf": { "type": "boolean", "default": false }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": ["string", "null"], "format": "date-time" }, "imageRefEtag": { "type": ["string", "null"] }, "videoRefEtag": { "type": ["string", "null"] } } }, "Project": { "type": "object", "properties": { "id": { "type": "integer" }, "subtitle": { "type": ["string", "null"] }, "start_date": { "type": ["string", "null"], "format": "date-time" }, "end_date": { "type": ["string", "null"], "format": "date-time" }, "venue": { "type": ["string", "null"] }, "images": { "type": "array", "items": { "$ref": "#/definitions/ImageRef" } }, "videos": { "type": "array", "items": { "$ref": "#/definitions/VideoRef" } }, "works": { "type": "array", "items": { "$ref": "#/definitions/Work" } }, "general": { "$ref": "#/definitions/GeneralSection" }, "generalId": { "type": "integer" } } }, "Tag": { "type": "object", "properties": { "id": { "type": "integer" }, "name": { "type": "string" }, "images": { "type": "array", "items": { "$ref": "#/definitions/ImageRef" } }, "videos": { "type": "array", "items": { "$ref": "#/definitions/VideoRef" } }, "general": { "type": "array", "items": { "$ref": "#/definitions/GeneralSection" } } } }, "SocialMedia": { "type": "object", "properties": { "id": { "type": "integer" }, "platform": { "type": ["string", "null"] }, "profileUrl": { "type": ["string", "null"] }, "username": { "type": ["string", "null"] }, "contactId": { "type": "integer" }, "Contact": { "anyOf": [ { "$ref": "#/definitions/Contact" }, { "type": "null" } ] } } }, "Contact": { "type": "object", "properties": { "id": { "type": "integer" }, "email": { "type": ["string", "null"] }, "socialmedia": { "type": "array", "items": { "$ref": "#/definitions/SocialMedia" } }, "userId": { "type": "integer" }, "Profile": { "anyOf": [ { "$ref": "#/definitions/Profile" }, { "type": "null" } ] } } }, "Profile": { "type": "object", "properties": { "userId": { "type": "integer" }, "html_statement": { "type": ["string", "null"] }, "html_additional": { "type": ["string", "null"] }, "portfolio_pdf_url": { "type": ["string", "null"], "default": "https://archive.org/download/jakubkanna_PORTFOLIO_2024/jakubkanna_PORTFOLIO_2024.pdf" }, "contact": { "type": "array", "items": { "$ref": "#/definitions/Contact" } }, "User": { "$ref": "#/definitions/User" } } }, "User": { "type": "object", "properties": { "id": { "type": "integer" }, "email": { "type": "string" }, "hash": { "type": "string" }, "salt": { "type": "string" }, "posts": { "type": "array", "items": { "$ref": "#/definitions/Post" } }, "Profile": { "type": "array", "items": { "$ref": "#/definitions/Profile" } } } }, "Work": { "type": "object", "properties": { "id": { "type": "integer" }, "dimensions": { "type": ["string", "null"] }, "year": { "type": ["integer", "null"] }, "images": { "type": "array", "items": { "$ref": "#/definitions/ImageRef" } }, "videos": { "type": "array", "items": { "$ref": "#/definitions/VideoRef" } }, "projects": { "type": "array", "items": { "$ref": "#/definitions/Project" } }, "general": { "$ref": "#/definitions/GeneralSection" }, "generalId": { "type": "integer" }, "createdAt": { "type": "string", "format": "date-time" }, "updatedAt": { "type": ["string", "null"], "format": "date-time" } } } }, "type": "object", "properties": { "generalSection": { "$ref": "#/definitions/GeneralSection" }, "imageRef": { "$ref": "#/definitions/ImageRef" }, "videoRef": { "$ref": "#/definitions/VideoRef" }, "post": { "$ref": "#/definitions/Post" }, "preferences": { "$ref": "#/definitions/Preferences" }, "project": { "$ref": "#/definitions/Project" }, "tag": { "$ref": "#/definitions/Tag" }, "socialMedia": { "$ref": "#/definitions/SocialMedia" }, "contact": { "$ref": "#/definitions/Contact" }, "profile": { "$ref": "#/definitions/Profile" }, "user": { "$ref": "#/definitions/User" }, "work": { "$ref": "#/definitions/Work" } } } ```
nickgros commented 1 month ago

There is a lot going on in this schema! I think it is going to take someone a while to figure out all of the things going on here.

I discovered one issue when I removed all properties in the schema other than the recursive anyOf arrays with refs:

Simplified / Broken schema ```json { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "ImageRef": { "type": "object", "properties": { "Preferences": { "anyOf": [ { "$ref": "#/definitions/Preferences" }, { "type": "null" } ] } } }, "VideoRef": { "type": "object", "properties": { "Preferences": { "anyOf": [ { "$ref": "#/definitions/Preferences" }, { "type": "null" } ] } } }, "Preferences": { "type": "object", "properties": { "homepage_background_image": { "default": null, "anyOf": [ { "$ref": "#/definitions/ImageRef" }, { "type": "null" } ] }, "homepage_background_video": { "default": null, "anyOf": [ { "$ref": "#/definitions/VideoRef" }, { "type": "null" } ] } } } }, "type": "object", "properties": { "imageRef": { "$ref": "#/definitions/ImageRef" }, "videoRef": { "$ref": "#/definitions/VideoRef" }, "preferences": { "$ref": "#/definitions/Preferences" } } } ```

The issue here is that RJSF is defaulting to the first anyOf option, which causes infinite recursion (either when it tries to generate the default form state, or when it builds the form).

You can break that cycle by settings the default value to null in the properties of preferences:

Simplified / fixed schema ```json { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { "ImageRef": { "type": "object", "properties": { "Preferences": { "anyOf": [ { "$ref": "#/definitions/Preferences" }, { "type": "null" } ] } } }, "VideoRef": { "type": "object", "properties": { "Preferences": { "anyOf": [ { "$ref": "#/definitions/Preferences" }, { "type": "null" } ] } } }, "Preferences": { "type": "object", "properties": { "homepage_background_image": { "default": null, "anyOf": [ { "$ref": "#/definitions/ImageRef" }, { "type": "null" } ] }, "homepage_background_video": { "default": null, "anyOf": [ { "$ref": "#/definitions/VideoRef" }, { "type": "null" } ] } } } }, "type": "object", "properties": { "imageRef": { "$ref": "#/definitions/ImageRef" }, "videoRef": { "$ref": "#/definitions/VideoRef" }, "preferences": { "$ref": "#/definitions/Preferences" } } } ```

However, fixing this issue in the base schema doesn't seem to totally fix the problem, so there is more to investigate.

jakubkanna commented 1 month ago

There is a lot going on in this schema! I think it is going to take someone a while to figure out all of the things going on here.

I discovered one issue when I removed all properties in the schema other than the recursive anyOf arrays with refs:

Simplified / Broken schema The issue here is that RJSF is defaulting to the first anyOf option, which causes infinite recursion (either when it tries to generate the default form state, or when it builds the form).

You can break that cycle by settings the default value to null in the properties of preferences:

Simplified / fixed schema However, fixing this issue in the base schema doesn't seem to totally fix the problem, so there is more to investigate.

Thanks for you reply, for now I will change my approach, it's hard to find out what's going on.

nickgros commented 1 month ago

I suspect it might work if you replaced all of your recursive schemas with anyOf defaulting to null. RJSF currently doesn't detect recursion when it builds the component tree, so it never terminates when it encounters a case that defaults to infinite recursion.

To fix this in RJSF, we would probably need to add recursion detection to the React component tree, and then bail when we encounter infinite recursion when we have no form data to show. In those cases, we could render a button to allow the user to render another level of the tree.

nickgros commented 1 month ago

A similar issue exists in the toIdSchema util where it doesn't handle the same types of recursive schemas that recurse infinitely 'by default'. This would also need to be fixed to properly support these schemas.