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.14k stars 2.18k forks source link

How to get `rawErrors` for parent schema or `errorSchema` in custom BaseInputTemplate #4271

Open alfonsoar opened 1 month ago

alfonsoar commented 1 month ago

Prerequisites

What theme are you using?

core

What is your question?

I am specifying a custom BaseInputTemplate and ObjectFieldTemplate in my project. My schema is complex enough that I have a nested properties inside of a allOf/if/then schema. See example: https://stackblitz.com/edit/vitejs-vite-srg8et?file=src%2FApp.tsx

In the example above, when you submit without entering any information you will see validation failed. But the inputs will not be in an error state since we do not have anything in the rawErrors prop being passed to the BaseInputTemplate.

How do I get the rawErrors from the parent schema (in this case it would be usernameAndPassword) in this field? Alternatively is it possible to pass the full errorSchema to the BaseInputTemplate instead?

note: I understand I can get the errorSchema from ErrorListTemplate but this means I would need to pass it down via context which I don't think is ideal.

If you switch the auth type to "SSO" and submit without entering information you will observe the input in a error state.

nickgros commented 1 month ago

If you look at the raw Ajv errors in the playground when you submit an empty username / password (pasted below), you will see that neither of the individual username nor password fields are marked as having an error. So RJSF has no idea that the errors should be applied to these fields in particular.

Some other options are to write custom logic and transform the errors to specifically mark the IDs of the fields where there are errors, or to restructure your schema to use oneOf instead of if/then. You could also use context, as you suggested, or even add custom logic to these fields. You have a lot of options, but unfortunately not all of them are straightforward or easy to implement to handle all cases.

Raw Ajv errors:

{
  "errors": [
    {
      "instancePath": "/authentication",
      "schemaPath": "#/properties/authentication/allOf/0/then/required",
      "keyword": "required",
      "params": {
        "missingProperty": "usernameAndPassword"
      },
      "message": "must have required property 'usernameAndPassword'",
      "schema": [
        "usernameAndPassword"
      ],
      "parentSchema": {
        "properties": {
          "usernameAndPassword": {
            "type": "object",
            "properties": {
              "username": {
                "type": "string",
                "title": "Username"
              },
              "password": {
                "type": "string",
                "title": "Password"
              }
            },
            "required": [
              "username",
              "password"
            ]
          }
        },
        "required": [
          "usernameAndPassword"
        ]
      },
      "data": {
        "credentialType": "username"
      }
    },
    {
      "instancePath": "/authentication",
      "schemaPath": "#/properties/authentication/allOf/0/if",
      "keyword": "if",
      "params": {
        "failingKeyword": "then"
      },
      "message": "must match \"then\" schema",
      "schema": {
        "properties": {
          "credentialType": {
            "const": "username"
          }
        }
      },
      "parentSchema": {
        "if": {
          "properties": {
            "credentialType": {
              "const": "username"
            }
          }
        },
        "then": {
          "properties": {
            "usernameAndPassword": {
              "type": "object",
              "properties": {
                "username": {
                  "type": "string",
                  "title": "Username"
                },
                "password": {
                  "type": "string",
                  "title": "Password"
                }
              },
              "required": [
                "username",
                "password"
              ]
            }
          },
          "required": [
            "usernameAndPassword"
          ]
        }
      },
      "data": {
        "credentialType": "username"
      }
    }
  ]
}
alfonsoar commented 1 month ago

Thanks nick for the response, I agree and don't think the issue is with RJSF itself. I also appreciate the additional suggestions you provided, below you will find some of my thoughts for each one. Let me know what you think.

write custom logic and transform the errors to specifically mark the IDs of the fields where there are errors

Did not know about this. Do you know why we don't pass schema to this function and we only pass uiSchema and errors ? If we passed the schema I could write a quick look up function that checks if the property for a given error has "children". For example, in this case if I see an error with the name required I could look up the property in the schema and If I see it has childrenm then generate two errors for those children.

Screenshot 2024-08-18 at 1 39 54 PM

to restructure your schema to use oneOf instead of if/then

Are you able to expand further how I could accomplish this? I already use oneOf in my schema but not following how I could accomplish what I need.

even add custom logic to these fields

I'm trying to find a "generic" solution and rather not tie my logic to these specific username/password fields. I am working on a library that is used for different use cases so having it tied to these to fields would be a PITA.

Finally, do you have any insight if we could pass the full errorSchema to the BaseInputTemplate ? ( I could help contribute this if it's a good idea)

abdalla-rko commented 1 month ago

I managed to make the ajv validation catch the individual username and password fields using dependencies. here is my schema:

{
  "type": "object",
  "required": [
    "authentication"
  ],
  "properties": {
    "authentication": {
      "title": "Authentication",
      "type": "object",
      "properties": {
        "credentialType": {
          "title": "Credential type",
          "type": "string",
          "default": "username",
          "oneOf": [
            {
              "const": "username",
              "title": "Username and password"
            },
            {
              "const": "secret",
              "title": "SSO"
            }
          ]
        }
      },
      "dependencies": {
        "credentialType": {
          "allOf": [
            {
              "if": {
                "properties": {
                  "credentialType": {
                    "const": "username"
                  }
                }
              },
              "then": {
                "properties": {
                  "usernameAndPassword": {
                    "type": "object",
                    "properties": {
                      "username": {
                        "type": "string",
                        "title": "Username"
                      },
                      "password": {
                        "type": "string",
                        "title": "Password"
                      }
                    },
                    "required": [
                      "username",
                      "password"
                    ]
                  }
                },
                "required": [
                  "usernameAndPassword"
                ]
              }
            },
            {
              "if": {
                "properties": {
                  "credentialType": {
                    "const": "secret"
                  }
                }
              },
              "then": {
                "properties": {
                  "sso": {
                    "type": "string",
                    "title": "SSO"
                  }
                },
                "required": [
                  "sso"
                ]
              }
            }
          ]
        }
      }
    }
  }
}

If you want to pass the full errorSchema to the BaseInputTemplate you need to build a custom ObjectField and ArrayField that passes the full errorSchema to its properties/items.

alfonsoar commented 1 month ago

Hi @abdalla-rko - thanks for the reply, I tried the new schema you provided in this playground but it does not appear to be working? What I'm I missing? https://stackblitz.com/edit/vitejs-vite-1ntklb?file=src%2FApp.tsx

If you want to pass the full errorSchema to the BaseInputTemplate you need to build a custom ObjectField and ArrayField that passes the full errorSchema to its properties/items.

Do you have any examples or pointers on how to do this? My understanding is you would need to use something like React. cloneElement in order to force new props onto the specific content element.

https://github.com/rjsf-team/react-jsonschema-form/blob/a7b25e8a1803149eccc4fd175ab6412d17cdf77c/packages/utils/src/types.ts#L638

abdalla-rko commented 1 month ago

Sorry, I gave you a schema without testing it on your playground, but it works on the rjsf playground. It seems to work on the rjsf playground because the formData editor is initially an empty object and it changes when the computedDefaults are set with the following value :

{
  "authentication": {
    "credentialType": "username"
  }
}

The formData editor will update the formData state which passed to the rjsf Fom and this triggers a componentDidUpdate in the form, which will try to get computedDefaults again. However now the difference is that it has the previous set computedDefault as formData (the json object above) and because of this the dependencies get resolved and formData will be set to:

{
  "authentication": {
    "credentialType": "username",
    "usernameAndPassword": {}
  }
}

So this is a bug in our computedDefault method in the getDefaultFormState file in Utils. I'll work on it and create a pr. For now you can do a workaround where you make formData observable and update the formData state after the componentDidMount which results in the same behavior as in the rjsf playground.

As for how to pass the full errorSchema to the BaseInputTemplate, sorry I have not tried this and I don't have any examples or pointers.

alfonsoar commented 5 days ago

Hi @abdalla-rko - Thanks for getting #4282 over the line, unfortunately it seems we have not fully fixed the issues with dependancies yet. I tested the new version (5.21.1) which includes your fix here: https://stackblitz.com/edit/vitejs-vite-ojdqpm?file=src%2FApp.tsx

You will notice that if you hit submit before interacting with the form the username and password will not be marked as error state. Looking at the formData I still see that userNameAndPassword are missing.

Let me know if you have any thoughts on this, i do plan to dig deeper into the issue but wanted to hear your thoughts since you are more familiar.

abdalla-rko commented 2 days ago

Hi @alfonsoar, you're still using @rjsf/utils v5.20.0(check package-lock.json). This is because you're not installing the latest @rjsf/utils and @rjsf/core has @rjsf/utils v^5.20.x as a peer dependency. I have tested it with @rjsf/utils v5.21.1 in your example and it works as expected. So just include @rjsf/utils it in your package.json.