Comportamentos automáticos
O framework aplica, de forma transparente, alguns comportamentos que independem de qualquer agregado específico — nenhuma classe precisa fazer nada para habilitá-los, mas é importante saber que existem:
- registro de beans;
- serialização de objetos de valor;
- gerenciamento de transações;
- validação das entidades JPA;
- open session in view desativado;
- CSRF no Swagger UI;
- expansão da consulta no Swagger UI;
- objetos de valor no Swagger UI.
Registro de beans
Implementações de IAgregadoRepositorio, IAgregadoAplicacaoRepositorio, IAgregadoServico e IAgregadoAplicacaoServico nunca precisam de @Component, @Service ou @Repository: AgregadoRegistrador, do módulo d5-framework-infraestrutura, varre os pacotes de autoconfiguração da aplicação — o pacote da classe anotada com @SpringBootApplication e seus subpacotes — em busca de classes concretas que implementem uma dessas quatro interfaces e não estejam anotadas, direta ou indiretamente, com @Component — o que inclui @Service e @Repository —, e registra cada uma como um bean com autowiring por construtor. Classes fora desses pacotes não são registradas.
O nome de cada bean é gerado a partir do nome completo da classe — por exemplo, com.example.exemplo.dominio.geografia.unidadefederativa.UnidadeFederativaServico#0 —, o que só importa quando outro bean precisa referenciá-lo pelo nome.
package dev.sauloaraujo.d5.framework.infraestrutura;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.stereotype.Component;
import dev.sauloaraujo.d5.framework.aplicacao.IAgregadoAplicacaoServico;
import dev.sauloaraujo.d5.framework.aplicacao.spi.IAgregadoAplicacaoRepositorio;
import dev.sauloaraujo.d5.framework.dominio.IAgregadoServico;
import dev.sauloaraujo.d5.framework.dominio.spi.IAgregadoRepositorio;
public class AgregadoRegistrador implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
var scanner = criarScanner();
var beanFactory = (BeanFactory) registry;
for (var pacoteBase : AutoConfigurationPackages.get(beanFactory)) {
for (var candidato : scanner.findCandidateComponents(pacoteBase)) {
registrar(candidato, registry);
}
}
}
private ClassPathScanningCandidateComponentProvider criarScanner() {
var scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addExcludeFilter(new AnnotationTypeFilter(Component.class));
scanner.addIncludeFilter(new AssignableTypeFilter(IAgregadoRepositorio.class));
scanner.addIncludeFilter(new AssignableTypeFilter(IAgregadoAplicacaoRepositorio.class));
scanner.addIncludeFilter(new AssignableTypeFilter(IAgregadoServico.class));
scanner.addIncludeFilter(new AssignableTypeFilter(IAgregadoAplicacaoServico.class));
return scanner;
}
private void registrar(BeanDefinition candidato, BeanDefinitionRegistry registry) {
var definicao = new GenericBeanDefinition();
definicao.setBeanClassName(candidato.getBeanClassName());
definicao.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_CONSTRUCTOR);
var nome = BeanDefinitionReaderUtils.generateBeanName(definicao, registry);
registry.registerBeanDefinition(nome, definicao);
}
}
No tutorial, UnidadeFederativaServico, UnidadeFederativaRepositorio, UnidadeFederativaAplicacaoServico e UnidadeFederativaAplicacaoRepositorio são registrados dessa forma — nenhum leva anotação alguma do Spring.
Serialização de objetos de valor
Um objeto de valor representado por uma única String — um código IBGE, uma sigla — trafega pela API como essa String, sem nenhum código de conversão no exemplo, em ambas as direções.
Na saída, JacksonConfiguracao registra um mixin que anota toString() com @JsonValue, fazendo o Jackson serializar a instância como o valor por trás dela, e não como um objeto com um atributo.
package dev.sauloaraujo.d5.framework.ui;
import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorStringAbstrato;
@Configuration
public class JacksonConfiguracao {
@Bean
JsonMapperBuilderCustomizer mixinObjetoValorString() {
return builder -> builder.addMixIn(ObjetoValorStringAbstrato.class, ObjetoValorStringMixin.class);
}
}
package dev.sauloaraujo.d5.framework.ui;
import com.fasterxml.jackson.annotation.JsonValue;
abstract class ObjetoValorStringMixin {
@JsonValue
@Override
public abstract String toString();
}
Na entrada, em parâmetros de caminho e de consulta, ConversaoConfiguracao registra ObjetoValorStringConversorFactory, que converte a String para qualquer subclasse de ObjetoValorStringAbstrato por reflexão, chamando seu construtor público de um único argumento String — uma subclasse sem esse construtor não pode ser recebida dessa forma. Se o construtor lançar uma exceção — por exemplo, porque o valor não atende ao formato de um ObjetoValorRegexAbstrato —, a fábrica a relança sem alteração; o serviço de conversão do Spring a embrulha em uma ConversionFailedException, que registra o tipo de destino da conversão, e o Spring MVC embrulha esta em uma MethodArgumentTypeMismatchException. ExcecaoManipulador procura na cadeia de causas a ConversionFailedException cujo tipo de destino é uma subclasse de ObjetoValorStringAbstrato e, encontrando nela a IllegalArgumentException, responde 422 Unprocessable Content, com a mensagem original. É essa verificação do tipo de destino que restringe a mensagem original aos objetos de valor: um parâmetro de outro tipo — um enum ou um número, por exemplo — também falha com uma IllegalArgumentException, lançada pela JDK, mas responde com uma mensagem fixa.
package dev.sauloaraujo.d5.framework.ui;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ConversaoConfiguracao {
@Bean
ObjetoValorStringConversorFactory objetoValorStringConversorFactory() {
return new ObjetoValorStringConversorFactory();
}
}
package dev.sauloaraujo.d5.framework.ui;
import java.lang.reflect.InvocationTargetException;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorStringAbstrato;
public class ObjetoValorStringConversorFactory implements ConverterFactory<String, ObjetoValorStringAbstrato> {
@Override
public <T extends ObjetoValorStringAbstrato> Converter<String, T> getConverter(Class<T> tipoAlvo) {
return source -> instanciar(tipoAlvo, source);
}
private <T extends ObjetoValorStringAbstrato> T instanciar(Class<T> tipoAlvo, String source) {
try {
return tipoAlvo.getConstructor(String.class).newInstance(source);
} catch (InvocationTargetException excecao) {
if (excecao.getCause() instanceof RuntimeException causa) {
throw causa;
}
throw new IllegalArgumentException("Não foi possível converter para " + tipoAlvo.getSimpleName(), excecao);
} catch (ReflectiveOperationException excecao) {
throw new IllegalArgumentException("Não foi possível converter para " + tipoAlvo.getSimpleName(), excecao);
}
}
}
No corpo JSON, a conversão de entrada é feita pelo próprio Jackson, sem configuração adicional: um objeto de valor é construído a partir da String através de seu construtor público de um único argumento String, e um agregado, através de seu construtor público, associando cada propriedade JSON ao parâmetro do construtor de mesmo nome. Essa associação depende de as classes serem compiladas com -parameters, o que o spring-boot-starter-parent, herdado através de d5-pai, já configura. Nesse caso, uma IllegalArgumentException lançada por um construtor chega a ExcecaoManipulador como causa de uma HttpMessageNotReadableException e resulta em 422 Unprocessable Content, com a mensagem original.
No tutorial, é por causa desse comportamento que o parâmetro identificador de UnidadeFederativaControlador é declarado como UnidadeFederativaCodigoIbge, e não como String, e que o corpo de uma requisição de inclusão ou alteração usa "identificador": "35" em vez de "identificador": {"valor": "35"}.
Gerenciamento de transações
Toda escrita sobre um agregado — incluir, alterar ou excluir — precisa ocorrer dentro de uma transação, para que as verificações feitas pelo serviço e a persistência propriamente dita sejam confirmadas ou desfeitas em conjunto. Em vez de anotar cada método de cada serviço ou repositório com @Transactional, o módulo d5-framework-ui-backend decide se abre uma transação a partir do verbo HTTP do endpoint: qualquer método de um controlador mapeado para POST — a convenção usada por AgregadoControladorAbstrato para incluir, alterar e excluir — ou mapeado com @RequestMapping sem verbo, que atende a todos eles, é envolvido por um advisor transacional; métodos mapeados para GET, como pesquisa e os formulários de inclusão, alteração e exclusão, não são.
A transação não impede, por si só, que duas requisições concorrentes passem pela mesma verificação de unicidade antes que qualquer uma delas grave: sob o nível de isolamento padrão dos bancos de dados usuais, ambas enxergam o estado anterior. Para garantir a unicidade sob concorrência, declare também a restrição no banco de dados — por exemplo, com unique = true na @Column da entidade JPA. A requisição que perder a corrida tem a gravação rejeitada pelo banco de dados e, como a que é rejeitada pelo serviço, responde 409 Conflict, embora com uma mensagem genérica, já que a mensagem do banco de dados não é exposta; veja ExcecaoManipulador.
package dev.sauloaraujo.d5.framework.ui.transacao;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.jspecify.annotations.Nullable;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.transaction.interceptor.DefaultTransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttribute;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
public class PostMappingTransactionAttributeSource implements TransactionAttributeSource {
private static final TransactionAttribute ESCRITA = new DefaultTransactionAttribute();
@Override
public @Nullable TransactionAttribute getTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
var metodoEspecifico = AopUtils.getMostSpecificMethod(method, targetClass);
var mapeamento = AnnotatedElementUtils.findMergedAnnotation(metodoEspecifico, RequestMapping.class);
if (mapeamento == null) {
return null;
}
var metodos = mapeamento.method();
var respondeAPost = metodos.length == 0 || Arrays.asList(metodos).contains(RequestMethod.POST);
return respondeAPost ? ESCRITA : null;
}
}
A configuração abaixo registra esse TransactionAttributeSource e o conecta a um TransactionInterceptor, aplicado a todo bean do contexto através de um BeanFactoryTransactionAttributeSourceAdvisor — dispensando a anotação @EnableTransactionManagement e qualquer @Transactional explícito. O advisor envolve cada controlador em um proxy que é uma subclasse dele; por isso, um controlador com métodos mapeados para POST não pode ser final — se for, a aplicação não inicia, com a mensagem Cannot subclass final class.
Como o DefaultAdvisorAutoProxyCreator é um BeanPostProcessor, ele e os beans de que depende são criados antes dos demais, quando nem todos os BeanPostProcessor estão registrados. Por isso, como na configuração de transações do próprio Spring, os métodos @Bean são estáticos e os beans são marcados com @Role(BeanDefinition.ROLE_INFRASTRUCTURE), e o TransactionInterceptor não recebe o gerenciador de transações: ele o obtém do contexto apenas na primeira chamada transacional. Assim, o gerenciador de transações e o EntityManagerFactory não são criados antecipadamente, e o Spring não registra avisos de beans que deixaram de ser processados por todos os BeanPostProcessor.
package dev.sauloaraujo.d5.framework.ui.transacao;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.transaction.interceptor.BeanFactoryTransactionAttributeSourceAdvisor;
import org.springframework.transaction.interceptor.TransactionAttributeSource;
import org.springframework.transaction.interceptor.TransactionInterceptor;
@Configuration(proxyBeanMethods = false)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class TransacaoConfiguracao {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
var autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
autoProxyCreator.setProxyTargetClass(true);
return autoProxyCreator;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static TransactionAttributeSource postMappingTransactionAttributeSource() {
return new PostMappingTransactionAttributeSource();
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static TransactionInterceptor postMappingTransactionInterceptor(
TransactionAttributeSource postMappingTransactionAttributeSource) {
var interceptor = new TransactionInterceptor();
interceptor.setTransactionAttributeSource(postMappingTransactionAttributeSource);
return interceptor;
}
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static BeanFactoryTransactionAttributeSourceAdvisor postMappingTransactionAdvisor(
TransactionAttributeSource postMappingTransactionAttributeSource,
TransactionInterceptor postMappingTransactionInterceptor) {
var advisor = new BeanFactoryTransactionAttributeSourceAdvisor();
advisor.setTransactionAttributeSource(postMappingTransactionAttributeSource);
advisor.setAdvice(postMappingTransactionInterceptor);
return advisor;
}
}
Validação das entidades JPA
O módulo d5-framework-infraestrutura depende de spring-boot-starter-validation, que traz o Hibernate Validator. Com ele no classpath, o Hibernate usa as anotações do Bean Validation de uma entidade JPA — como @NotNull e @Size — de duas formas, sem nenhuma configuração na aplicação:
- ao gerar o esquema do banco de dados,
@NotNulltorna a colunanot null— inclusive a de um atributo de tipo primitivo, que, sem a anotação, o Hibernate declara anulável — e omaxde@Sizedefine seu tamanho, dispensandonullableelengthna@Column; - antes de cada
inserteupdate, valida a entidade e, se alguma restrição for violada, lançaConstraintViolationExceptionsem enviar o comando ao banco de dados — o que independe do banco e do driver JDBC.ExcecaoManipuladorresponde 422 Unprocessable Content, com a mensagem de cada violação precedida da propriedade da entidade — por exemplo,nome: tamanho deve ser entre 0 e 100.
Para entidades cujo identificador é atribuído pela aplicação, a validação ocorre na confirmação da transação, e não na chamada a save; nesse caso, a exceção chega a ExcecaoManipulador como causa de uma TransactionSystemException.
O Hibernate Validator escolhe o idioma das mensagens pela localidade padrão da JVM. Para que as mensagens fiquem em português, como as demais respostas do backend, independentemente do servidor, ValidacaoConfiguracao fornece ao Hibernate uma ValidatorFactory com a localidade pt-BR.
Essas anotações expressam apenas as restrições da persistência — como o tamanho de uma coluna —, e não as regras do domínio, que o agregado e seus objetos de valor já garantem; veja Entidades JPA.
No tutorial, UnidadeFederativaJpa declara dessa forma as restrições de suas colunas.
Open session in view desativado
O Spring Boot ativa por padrão o open session in view: um EntityManager permanece aberto durante toda a requisição HTTP, permitindo que associações preguiçosas sejam carregadas depois do serviço, inclusive durante a serialização da resposta — e registra um aviso ao iniciar a aplicação quando a propriedade spring.jpa.open-in-view não é definida. Uma aplicação D5 não precisa desse padrão: as escritas ocorrem em uma transação que termina com o método do controlador, as pesquisas devolvem projeções, e não entidades JPA, e as demais leituras devolvem agregados, convertidos das entidades JPA ainda no repositório. Uma entidade JPA pode ter uma associação preguiçosa com a de outro agregado, mas o repositório só lê dela o identificador, que o proxy do Hibernate fornece sem carregar a entidade associada — e, portanto, sem precisar do EntityManager aberto; veja a entidade JPA do cliente no tutorial.
Por isso, o módulo d5-framework-ui-backend define false como valor padrão dessa propriedade através de JpaEnvironmentPostProcessor, registrado em META-INF/spring.factories pelo mesmo motivo que SpringDocEnvironmentPostProcessor. Como a propriedade é acrescentada com a menor precedência, qualquer outra fonte de configuração a redefine — por exemplo, spring.jpa.open-in-view=true no application.properties da aplicação.
package dev.sauloaraujo.d5.framework.ui;
import java.util.Map;
import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
public class JpaEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
environment.getPropertySources()
.addLast(new MapPropertySource("d5-framework-ui-backend", Map.of("spring.jpa.open-in-view", false)));
}
}
CSRF no Swagger UI
Toda operação de escrita exposta pelo backend exige um token de CSRF, enviado pelo cliente através do cabeçalho X-XSRF-TOKEN — mas o Swagger UI só anexa esse cabeçalho automaticamente às requisições quando a propriedade springdoc.swagger-ui.csrf.enabled está true. O módulo d5-framework-springdoc define esse valor como padrão através de um EnvironmentPostProcessor, dispensando essa configuração em quem depende dele.
package dev.sauloaraujo.d5.framework.springdoc;
import java.util.Map;
import org.springframework.boot.EnvironmentPostProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
public class SpringDocEnvironmentPostProcessor implements EnvironmentPostProcessor {
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
environment.getPropertySources().addLast(
new MapPropertySource("d5-framework-springdoc", Map.of("springdoc.swagger-ui.csrf.enabled", true)));
}
}
Veja d5-framework-springdoc para mais detalhes, incluindo por que esse comportamento é registrado em META-INF/spring.factories, e não em um arquivo .imports. O cookie que esse cabeçalho reflete só existe porque FiltroCookieCsrf força sua criação antecipadamente — já na resposta à página do Swagger UI. O Swagger UI lê o cookie a cada requisição, e não apenas ao carregar a página; mas a resposta à primeira requisição autenticada apaga o cookie, porque o Spring Security renova o token de CSRF ao autenticar o usuário, e só a resposta seguinte traz um novo. Sem o cookie ou sem o cabeçalho, uma requisição de escrita é rejeitada com 403 e a mensagem Token CSRF ausente ou inválido.; veja ManipuladorAcessoNegado.
Expansão da consulta no Swagger UI
pesquisa de AgregadoControladorAbstrato recebe a consulta através de um parâmetro anotado com @ModelAttribute, que o Spring MVC associa a partir de parâmetros de requisição individuais — nome, ordenacao, descendente, indicePagina e tamanhoPagina, por exemplo — e não de um único parâmetro serializado em JSON. Sem nenhum ajuste, o Swagger UI documentaria pesquisa com um único parâmetro consulta, do tipo objeto, editável como uma caixa de texto JSON — que pareceria funcional, mas cujo conteúdo o Spring MVC nunca chega a interpretar.
O módulo d5-framework-springdoc corrige essa representação com um OperationCustomizer que localiza, em cada operação, os parâmetros de método anotados com @ModelAttribute e os substitui, na documentação, pelos parâmetros individuais de suas propriedades — refletindo exatamente os parâmetros que o Spring MVC de fato associa. Por atuar apenas sobre parâmetros @ModelAttribute, o customizador não afeta nenhum outro parâmetro — em particular, um futuro endpoint que realmente precise receber um objeto serializado em JSON por parâmetro de consulta permanece intocado.
package dev.sauloaraujo.d5.framework.springdoc;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Optional;
import org.springdoc.core.customizers.OperationCustomizer;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.method.HandlerMethod;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverters;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
class ModelAttributeCustomizador implements OperationCustomizer {
@Override
public Operation customize(Operation operacao, HandlerMethod handlerMethod) {
if (operacao.getParameters() == null) {
return operacao;
}
for (var parametroMetodo : handlerMethod.getMethodParameters()) {
if (parametroMetodo.hasParameterAnnotation(ModelAttribute.class)) {
expandir(operacao, Objects.requireNonNull(parametroMetodo.getParameterName()),
parametroMetodo.getParameterType());
}
}
return operacao;
}
private void expandir(Operation operacao, String nomeParametro, Class<?> tipoParametro) {
var parametroOriginal = remover(operacao, nomeParametro);
if (parametroOriginal.isEmpty()) {
return;
}
var esquema = ModelConverters.getInstance().resolveAsResolvedSchema(new AnnotatedType(tipoParametro)).schema;
if (esquema == null || esquema.getProperties() == null) {
operacao.getParameters().add(parametroOriginal.get());
return;
}
var propriedadesObrigatorias = esquema.getRequired();
esquema.getProperties().forEach((nome, esquemaPropriedade) -> {
var parametroExpandido = new Parameter().name((String) nome).in(parametroOriginal.get().getIn())
.schema((Schema<?>) esquemaPropriedade)
.required(propriedadesObrigatorias != null && propriedadesObrigatorias.contains(nome));
operacao.getParameters().add(parametroExpandido);
});
}
private Optional<Parameter> remover(Operation operacao, String nomeParametro) {
var parametros = new ArrayList<>(operacao.getParameters());
var parametroOriginal = parametros.stream()
.filter(parametro -> Objects.equals(parametro.getName(), nomeParametro)).findFirst();
parametroOriginal.ifPresent(parametros::remove);
operacao.setParameters(parametros);
return parametroOriginal;
}
}
Veja d5-framework-springdoc para como esse customizador é registrado.
Objetos de valor no Swagger UI
Os objetos de valor de string são serializados em JSON como uma string simples, conforme descrito em serialização de objetos de valor. O springdoc-openapi não conhece essa configuração do Jackson e, sem ajuste, documentaria esses tipos a partir de seus getters: um identificador com getUnidadeFederativa(), como o código IBGE de um município, apareceria no Swagger UI como um objeto { "unidadeFederativa": "string" }, em vez de uma string.
O módulo d5-framework-springdoc corrige essa representação com um ModelConverter que descreve como string todo tipo que estende ObjetoValorStringAbstrato.
package dev.sauloaraujo.d5.framework.springdoc;
import java.util.Iterator;
import org.jspecify.annotations.Nullable;
import dev.sauloaraujo.d5.framework.dominio.impl.ObjetoValorStringAbstrato;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.core.converter.ModelConverter;
import io.swagger.v3.core.converter.ModelConverterContext;
import io.swagger.v3.core.util.Json;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
/**
* Objetos de valor de string são serializados como string simples; portanto, a
* documentação não deve expor suas propriedades.
*/
class ObjetoValorStringConversor implements ModelConverter {
@Override
public @Nullable Schema<?> resolve(AnnotatedType tipo, ModelConverterContext contexto,
Iterator<ModelConverter> cadeia) {
var tipoJava = Json.mapper().constructType(tipo.getType());
if (tipoJava != null && ObjetoValorStringAbstrato.class.isAssignableFrom(tipoJava.getRawClass())) {
return new StringSchema();
}
return cadeia.hasNext() ? cadeia.next().resolve(tipo, contexto, cadeia) : null;
}
}
Veja d5-framework-springdoc para como esse conversor é registrado.