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.