Render AJV errors outside of JsonForms

Hi all,

I have a fairly complex schema with a lot of nesting and it’s often hard to work out where all the errors in the nested objects are. To try and solve this problem I was hoping to introduce a component to render all errors from all levels and hopefully a hyperlink to each error that lives at the same level as my JsonForm component. For example:

            <MyCustomErrorRenderer errors={errors}/>
            <JsonForms
                schema={props.schema}
                data={props.data}
                uischema={props.uischema}
                renderers={props.renderers}
                cells={props.cells}
                onChange={({data}) => onChange(data)}
                ajv={ajv}
            ></JsonForms>

Where errors in my custom component comes from the ajv validation errors coming from jsonforms.
I tried ajv.errors and this was always null even when there were errors in my data, I suspect because the ajv passed in to JsonForms is copied? I also tried

    const ctx = useJsonForms();
    const ajvErorrs = ctx.core?.errors

But I guess this would only work within a custom renderer?

Any ideas about how I can get the errors within JsonForms without revalidating myself? Or any other better ideas about how to do what I want :slight_smile:

Hi @james-morris!

The handed over ajv should also be the one which is used. However we not only use it to validate the handed over schemas, but also the rule-schemas and oneOf subschemas etc. etc.

The onChange handler does not only receive data but it also receives the AJV errors. So you can just track them via the onChange call and be sure that they fit exactly to the data. Note that the errors emitted by JSON Forms before 3.0.0 are slightly modified from the AJV standard (I think we convert the path to a different format) while starting with 3.0.0 we return the AJV errors as we receive them.

1 Like

Is it possible to use the error.datapath as a hyperlink that jumps to the section of the UI where the error is happening, expanding any collapsed array objects along the way?

Hi @james-morris, sure, this can be implemented. From the top of my head you would need to have custom renderers for all expand panels on the way. With the default renderer set these are the group and array layout renderers. For them you can then implement any arbitrary expand behavior you like. You can inject the knowledge about which error path was selected via your own React contexts.

Hi @sdirix . Sounds good, i’ll give that a go. one question I have is how I should call the expand function from outside the array renderer for cases where I have errors hidden away in array objects that are not expanded?

Hi @james-morris. Any communication from outside of JSON Forms to and from your custom renderers can be handled by implementing your own React context.

For example you can always inject the current selected error via your own context into the custom array renderer. Whenever that selected error changes, it can then expand / collapse its items how it seems fit.

Hi @sdirix i’ve been working on this for a little while and I think i’ve got really close to a solution but wanted to check that a) I understood your suggestions correctly and b) if you have any ideas of how to solve my final issue.

I’ve created a context with two attributes, one is a string that contains the path to my error (error.dataPath) and the other is a function that updates that error.dataPath

export const SelectedErrorContext = React.createContext({
    selectedError: "",
    setSelectedError: (error: string) => {}
});

my top level component that wraps both my errors header bar and the JSON forms looks like this:

function ConfigPage(props: JsonFormsInitStateProps & JsonFormsReactProps & DataModifierProps): JSX.Element {
    const [ajvErrors, setAjvErrors] = useState<ErrorObject[]>([])
    const [selectedError, setSelectedError] = useState<string>("");

    const ajv: AJV.Ajv = createAjv(
        {
            $data: true
        }
    )

    function onChange(data: RtengViBatchSolutionFactoryConfig, errors: ErrorObject[]){
        setAjvErrors(errors);
        props.parentCallback(data);
    }

    return (
        <SelectedErrorContext.Provider value={{selectedError: selectedError, setSelectedError: setSelectedError}}>
            <ErrorHeader errors={ajvErrors}/>
            <JsonForms
                schema={props.schema}
                data={props.data}
                uischema={props.uischema}
                renderers={props.renderers}
                cells={props.cells}
                onChange={({data, errors}) => onChange(data, errors)}
                ajv={ajv}
            ></JsonForms>
        </SelectedErrorContext.Provider>
    );
}

In order to determine whether to expand my Custom Array Layout Renderer I use the error path in the context.

    isExpanded = (index: number) =>
        (this.state.expanded === composePaths(this.props.path, `${index}`) || this.context.selectedError.indexOf(composePaths(this.props.path, `${index}`)) !== -1)

My ErrorHeader that contains the links to each of the errors has a custom scrolling function that first updates the context value (expanding any collapsed array objects) and then attempts to scroll down to the control containing the error:

        const onScroll = (element: HTMLElement) => {
            setSelectedError(error.dataPath);
            const yCoordinate = element.getBoundingClientRect().top + window.pageYOffset;
            const yOffset = -80;
            window.scrollTo({ top: yCoordinate + yOffset, behavior: 'smooth' });
        }

The problem i’m facing is that my custom scroll function doesn’t wait for the rest of the app to rerender following the context update before it attempts to scroll down and so the scrolling happens before the opening of the array objects and can result in it scrolling to the wrong place.

Any thoughts or suggestions on the above much appreciated.

Hi @james-morris, overall this looks pretty much as I expected :+1:

How is your onScroll method invoked? I think the easiest solution would be to just invoke it after a set timeout once you assume that the expanding of your element is finished (e.g. after 100ms?). Actually looking at your scrolling method: How do you actually determine the right HTML element?

If you like a “cleaner” solution you have multiple approaches available to you: For example you could check for ongoing animations and only then scroll once nothing is expanding/collapsing anymore. Alternatively the scrolling logic could be moved into the expanding array items and could then for example be performed by the closest item once it knows that its animation finished. Another option is to move the scrolling logic into the field which shows the error once it realizes that the selected error is its own (could then require a timeout again)

Whether these solutions are actually better in practice is debatable and each comes with its own downsides. I would probably go with the simple timeout approach if there are no additional requirements.

Unrelated to the discussed topic, but you should not create a new AJV instance with each rerendering. This will lead to at least an additional validation pass each time the ConfigPage is rendered. I would like to suggest to either making it a “global” by creating it outside of the component or to memoize it via useMemo.

Hi @sdirix, it’s been a while since i’ve been able to focus on this but i’m back to looking at it again.

To answer your first question on how I determine the right HTML element. I’ve changed the Id of each of the Material Input Renderers so that it uses the path instead of the id in the props. The path is unique and also aligns with the path that ajv returns. This is a bit of a pain since i have to make a whole bunch of custom renderers just to make one small change that enables this functionality which makes upgrading a bit of a pain in the future. Can you think of a better solution to this?

With regards to your suggestion about AJV, i’ve taken your advise and made it global, thanks for that!

i’ve recently upgraded to 3.0alpha and another thing I noticed is that the ErrorObject interface that I guess was being provided indirectly by JSON forms was different to the one I received at runtime from AJV. The difference was that the interface I had access to whilst writing the code had an attribute called dataPath whereas at runtime the attribute that seemed most similar was called instancePath.

Hi @james-morris,

To answer your first question on how I determine the right HTML element. I’ve changed the Id of each of the Material Input Renderers so that it uses the path instead of the id in the props. The path is unique and also aligns with the path that ajv returns. This is a bit of a pain since i have to make a whole bunch of custom renderers just to make one small change that enables this functionality which makes upgrading a bit of a pain in the future. Can you think of a better solution to this?

Yes, at the moment there is no better way to “mix-in” functionality as to reregistering all existing renderers with your custom bindings. There is surely a way to enhance the base renderer sets to allow for something like this but all would require a lot of effort.

For now what you do is the way to go. If you don’t just copy our bindings and modify them but just call them and add your customization on top, this should be relatively future proof in require minimal adjustments when upgrading, if any.

i’ve recently upgraded to 3.0alpha and another thing I noticed is that the ErrorObject interface that I guess was being provided indirectly by JSON forms was different to the one I received at runtime from AJV. The difference was that the interface I had access to whilst writing the code had an attribute called dataPath whereas at runtime the attribute that seemed most similar was called instancePath.

We updated to AJV v8 in the mean time. A lot of tooling (e.g. for older Angular or even the current Vue) still install AJV v6 by default which takes precedence over our AJV v8. Therefore you then implement against the v6 typings but at runtime JSON Forms will correctly receive AJV v8 which produces different error objects.