swagger-api / swagger-core

Examples and server integrations for generating the Swagger API Specification, which enables easy access to your REST API
http://swagger.io
Apache License 2.0
7.37k stars 2.17k forks source link

Support subTypes registered via mapper.registerSubtypes() #4225

Open ivan-zaitsev opened 2 years ago

ivan-zaitsev commented 2 years ago

Sometimes it is not possible to use annotatios (for example subtypes should be in different modules).

By default ModelConverters uses new ModelResolver(Json.mapper()). ModelResolver uses mapper.getSerializationConfig().getAnnotationIntrospector().findSubtypes(parentClass).

Jackson implementation of mapper.getSerializationConfig().getAnnotationIntrospector().findSubtypes() returns subtypes relying on @JsonSubTypes annotation of parent class, but not include subtypes added manually using mapper.registerSubtypes().

To include subtypes added manually - class ModelResolver might be changed to use

Class<?> parentClass = ?
SerializationConfig config = mapper.getSerializationConfig();
BeanDescription parentDescriptor = config.introspectClassAnnotations(parentClass);
List<NamedType> subtypes = mapper.getSubtypeResolver().collectAndResolveSubtypesByClass(config, parentDescriptor.getClassInfo());

instead of

Class<?> parentClass = ?
BeanDescription parentDesc = _mapper.getSerializationConfig().introspectClassAnnotations(parentClass);
List<NamedType> subTypes =_intr.findSubtypes(parentDesc.getClassInfo());

Also to achieve registering subtypes using mapper.registerSubtypes() configuring of ObjectMapper for ModelResolver should be implemented.

ivan-zaitsev commented 2 years ago

It is also possible to implement similar logic on client side, but it uses reflection and is not so good as native implementation.

registerSubtypes(Subtype.class, ...);

private void registerSubtypes(Class<?>... classes) {
    ObjectMapper mapper = new ObjectMapper();
    mapper.setAnnotationIntrospector(new ExtendedJacksonAnnotationIntrospector(mapper));
    mapper.registerSubtypes(Subtype.class);
    new ModelConverterRegistrar().register(new ModelResolver(mapper));
}
import java.util.List;

import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedClass;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
import com.fasterxml.jackson.databind.jsontype.NamedType;

public class ExtendedJacksonAnnotationIntrospector extends JacksonAnnotationIntrospector {

    private final ObjectMapper mapper;

    public ExtendedJacksonAnnotationIntrospector(ObjectMapper mapper) {
        this.mapper = mapper;
    }

    @Override
    public List<NamedType> findSubtypes(Annotated baseType) {
        DeserializationConfig config = mapper.getDeserializationConfig().with(new JacksonAnnotationIntrospector()); 
        return (List<NamedType>) mapper.getSubtypeResolver().collectAndResolveSubtypesByClass(config, (AnnotatedClass) baseType);
    }

}
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.core.jackson.ModelResolver;

public class ModelConverterRegistrar {

    private static final ModelConverters MODEL_CONVERTERS_INSTANCE = ModelConverters.getInstance();

    public void register(ModelResolver modelResolver) { 
        findRegisteredConverterSameAs(modelResolver).ifPresent(MODEL_CONVERTERS_INSTANCE::removeConverter);
        MODEL_CONVERTERS_INSTANCE.addConverter(modelResolver);
    }

    private Optional<ModelConverter> findRegisteredConverterSameAs(ModelConverter modelConverter) {
        return findRegisteredConverters().stream()
                .filter(registeredModelConverter -> modelConverter.getClass().equals(registeredModelConverter.getClass()))
                .findFirst();
    }

    @SuppressWarnings("unchecked")
    private List<ModelConverter> findRegisteredConverters() {       
        try {
            Field field = MODEL_CONVERTERS_INSTANCE.getClass().getDeclaredField("converters");
            field.setAccessible(true);
            return (List<ModelConverter>) field.get(MODEL_CONVERTERS_INSTANCE);
        } catch (Exception e) {
            return Collections.emptyList();
        }
    }

}

If you are using Spring you can just create bean of ModelResolver and ModelConverterRegistrar will be used by Spring.

benjaminpochat commented 1 year ago

Hi,

I have the same problem, and tried to use the ExtendedJacksonAnnotationIntrospector as suggested above. For your information, this workaround did not work in my context. I try to produce an openapi.json with the following model :

// no JsonType annotation, no need to be in the openapi.json
public interface RootInterface {}

Referenced in a REST resource, to include in openapi.json :

@JsonTypeName("IntermediateInterface ")
public interface IntermediateInterface extends RootInterface {}

In a seperate module, to include in openapi.json as well :

@JsonTypeName("ImplementationClass")
public class ImplementationClass implements IntermediateInterface  {...}

While IntermediateInterface is being resolved, we can see in ModelResolver#resolveSubtypes that the subtype ImplementationClass is found at the begining. But it is removed by the method ModelResolver#removeSuperClassAndInterfaceSubTypes, with the following explaination :

    /**
     * As the introspector will find @JsonSubTypes for a child class that are present on its super classes, the
     * code segment below will also run the introspector on the parent class, and then remove any sub-types that are
     * found for the parent from the sub-types found for the child. The same logic all applies to implemented
     * interfaces, and is accounted for below.
     */

I am not sure to understand well this explaination, but it seems that ModelResolver#resolveSubtypes expects some @JsonSubTypes annotations that the solution suggested in the previous message does not provide.