JAXB validation using annotations

20,527

Solution 1

Good question. As far as I know, the required attribute is generated by XJC when it finds a non-optional schema type, and I think it's also used by the schema generator. At runtime, though, it's not used for anything, serving no other purpose than a documentary annotation.

One thing you could consider is the JAXB runtime's callback options. In this case, you could just define a afterUnmarshal() method on MyClass which programmatically validates the state of the object, throwing an exception if it doesn't like it. See the above link for other options, including registering separate validator classes.

Having said that, validation against a schema really is the best way. If you don't have one, you should considering writing one. The schemagen tool can generate a schema from your object model, which you can then modify to add whatever constraints you like. Hopefully, schemagen will generate mandatory schema elements from your required=true class fields.

Solution 2

Great question, especially considering popularity of object-first, schema-never development. I too would like to validate objects by leveraging the existing annotations prior to marshaling.

Whilst either we wait for JAXB-430, or become accepted contributors to Java, what follows is an extremely limited home-grown attempt regarding only XmlElement(required=true}. Note this will not work with non-default security policy because of Field.setAccessible().

Use Case Test

import javax.xml.bind.annotation.XmlElement;
import JaxbValidator.ValidationException;
import org.testng.annotations.Test;

public class JaxbValidatorTest {

    static class Llama {
        @XmlElement(required = false)
        private final String no;

        @XmlElement(required = true)
        private final String yes;

        public Llama(String no, String yes) {
            super();
            this.no = no;
            this.yes = yes;
        }
    }
    @Test
    public void validateRequired() {
        try {
            Llama o = new Llama("a", "b");
            // THE MAIN EVENT - see if 'required' is honored
            JaxbValidator.validateRequired(o, Llama.class);
        } catch (ValidationException e) {
            assert false : "Should not have thrown validation exception.";
        }
        try {
            Llama o = new Llama(null, null);
            // Again - see if 'required' is honored
            JaxbValidator.validateRequired(o, Llama.class);
            assert false : "Should have thrown validation exception for 'yes'";
        } catch (ValidationException e) {
            assert e.getMessage() != null: "Expected validation message, got null." ;
        }
    }
}

Implementation

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import javax.xml.bind.annotation.XmlElement;
import org.apache.log4j.Logger;

/**
 * oh so minimal consideration of JAXB annotation
 */
public class JaxbValidator {

    private static final Logger LOG = Logger.getLogger(JaxbValidator.class);

    public static class ValidationException extends Exception {
        public ValidationException(String message, Throwable cause) {
            super(message, cause);
        }
        public ValidationException(String message) {
            super(message);
        }
    }

    /**
     * Enforce 'required' attibute.
     *
     * Requires either no security manager is used or the default security manager is employed. 
     * @see {@link Field#setAccessible(boolean)}.
     */
    public static <T> void validateRequired(T target, Class<T> targetClass)
        throws ValidationException {
        StringBuilder errors = new StringBuilder();
        Field[] fields = targetClass.getDeclaredFields();
        for (Field field : fields) {
            XmlElement annotation = field.getAnnotation(XmlElement.class);
            if (annotation != null && annotation.required()) {
                try {
                    field.setAccessible(true);
                    if (field.get(target) == null) {
                        if (errors.length() != 0) {
                            errors.append(" ");
                        }
                        String message = String.format("%s: required field '%s' is null.",
                                                       targetClass.getSimpleName(),
                                                       field.getName());
                        LOG.error(message);
                        errors.append(message);
                    }
                } catch (IllegalArgumentException e) {
                    LOG.error(field.getName(), e);
                } catch (IllegalAccessException e) {
                    LOG.error(field.getName(), e);
                }
            }
        }
        if (errors.length() != 0) {
            throw new ValidationException(errors.toString());
        }
    }

And yes ... it doesn't do deep inspection. I wasn't sure if JAXB handles cyclic graphs, so I didn't attempt recursion without knowing if that had to be dealt with. I shall save that for a dear reader or next edit.

Share:
20,527
Martin
Author by

Martin

Updated on January 14, 2020

Comments

  • Martin
    Martin over 4 years

    If I have a simple class such as:-

    @XmlRootElement
    public class MyClass
    {
       @XmlAttribute(required=true)
       private String myattribute
    }
    

    Is it possible to validate a corresponding xml document WITHOUT an xml schema i.e. using only the annotations?