Java tutorial
/* * Copyright 2016 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.api.server.spi.config.jsonwriter; import com.google.api.server.spi.Constant; import com.google.api.server.spi.EndpointMethod; import com.google.api.server.spi.ObjectMapperUtil; import com.google.api.server.spi.TypeLoader; import com.google.api.server.spi.config.ApiConfigException; import com.google.api.server.spi.config.ApiConfigWriter; import com.google.api.server.spi.config.ResourcePropertySchema; import com.google.api.server.spi.config.ResourceSchema; import com.google.api.server.spi.config.annotationreader.ApiAnnotationIntrospector; import com.google.api.server.spi.config.model.ApiAuthConfig; import com.google.api.server.spi.config.model.ApiCacheControlConfig; import com.google.api.server.spi.config.model.ApiConfig; import com.google.api.server.spi.config.model.ApiFrontendLimitsConfig; import com.google.api.server.spi.config.model.ApiKey; import com.google.api.server.spi.config.model.ApiMethodConfig; import com.google.api.server.spi.config.model.ApiNamespaceConfig; import com.google.api.server.spi.config.model.ApiParameterConfig; import com.google.api.server.spi.config.model.SchemaRepository; import com.google.api.server.spi.config.model.Types; import com.google.api.server.spi.config.scope.AuthScopeExpressions; import com.google.api.server.spi.config.validation.ApiConfigValidator; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.collect.Iterables; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.common.collect.Multimaps; import com.google.common.reflect.TypeToken; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import java.io.IOException; import java.lang.reflect.Method; import java.lang.reflect.TypeVariable; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import javax.annotation.Nullable; /** * Writer of legacy Endpoints API configurations. */ public class JsonConfigWriter implements ApiConfigWriter { /** * Default deadline to set in lily adapters. Use a default deadline for Swarm APIs that is * slightly larger than the GAE request processing limits (60 seconds). This value is also used as * the Harpoon service deadline when we reach BEs outside Google. */ private static final double DEFAULT_LILY_DEADLINE = 65.0; public static final String MAP_SCHEMA_NAME = "JsonMap"; public static final String ANY_SCHEMA_NAME = "_any"; private final TypeLoader typeLoader; private final ApiConfigValidator validator; private final ResourceSchemaProvider resourceSchemaProvider = new JacksonResourceSchemaProvider(); public JsonConfigWriter() throws ClassNotFoundException { this.typeLoader = new TypeLoader(JsonConfigWriter.class.getClassLoader()); this.validator = new ApiConfigValidator(typeLoader, new SchemaRepository(typeLoader)); } public JsonConfigWriter(TypeLoader typeLoader, ApiConfigValidator validator) throws ClassNotFoundException { this.typeLoader = typeLoader; this.validator = validator; } private static final ObjectMapper objectMapper = ObjectMapperUtil.createStandardObjectMapper(); @Override public Map<ApiKey, String> writeConfig(Iterable<? extends ApiConfig> configs) throws ApiConfigException { Multimap<ApiKey, ? extends ApiConfig> apisByKey = Multimaps.index(configs, new Function<ApiConfig, ApiKey>() { @Override public ApiKey apply(ApiConfig config) { return config.getApiKey(); } }); // This *must* retain the order of apisByKey so the lily_java_api BUILD rule has predictable // output order. Map<ApiKey, String> results = Maps.newLinkedHashMap(); for (ApiKey apiKey : apisByKey.keySet()) { Collection<? extends ApiConfig> apiConfigs = apisByKey.get(apiKey); validator.validate(apiConfigs); results.put(apiKey, generateForApi(apiConfigs)); } return results; } @Override public String getFileExtension() { return "api"; } private String generateForApi(Iterable<? extends ApiConfig> apiConfigs) throws ApiConfigException { ObjectNode root = objectMapper.createObjectNode(); // First, generate api-wide configuration options, given any ApiConfig. ApiConfig apiConfig = Iterables.get(apiConfigs, 0); convertApi(root, apiConfig); convertApiAuth(root, apiConfig.getAuthConfig()); convertApiFrontendLimits(root, apiConfig.getFrontendLimitsConfig()); convertApiCacheControl(root, apiConfig.getCacheControlConfig()); convertApiNamespace(root, apiConfig.getNamespaceConfig()); // Next, generate config-specific configuration options, convertApiMethods(apiConfigs, root); return toString(root); } /** Writes an object node as a string. */ private String toString(ObjectNode node) throws ApiConfigException { try { return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(node); } catch (IOException e) { throw new ApiConfigException(e); } } private void setNodePropertyNoConflict(ObjectNode node, String key, JsonNode value, String errorMessage) { if (!node.path(key).isMissingNode()) { throw new IllegalArgumentException(errorMessage); } node.set(key, value); } private void setNodePropertyNoConflict(ObjectNode node, String key, JsonNode value) { setNodePropertyNoConflict(node, key, value, "Multiple values for same key '" + key + "'"); } private void convertApi(ObjectNode root, ApiConfig config) { root.put("extends", getParentApiFile()); root.put("abstract", config.getIsAbstract()); root.put("root", config.getRoot()); root.put("name", config.getName()); if (config.getCanonicalName() != null) { root.put("canonicalName", config.getCanonicalName()); } root.put("version", config.getVersion()); if (config.getTitle() != null) { root.put("title", config.getTitle()); } if (config.getDescription() != null) { root.put("description", config.getDescription()); } if (config.getDocumentationLink() != null) { root.put("documentation", config.getDocumentationLink()); } root.put("defaultVersion", config.getIsDefaultVersion()); ArrayNode discovery = objectMapper.createArrayNode(); discovery.add(config.getIsDiscoverable() ? "PUBLIC" : "OFF"); root.set("discovery", discovery); ObjectNode adapter = objectMapper.createObjectNode(); adapter.put("bns", config.getBackendRoot()); adapter.put("deadline", DEFAULT_LILY_DEADLINE); adapter.put("type", "lily"); root.set("adapter", adapter); } /** * Returns the name of the file the config should extend. Subclasses may * override to use their own api file. */ protected String getParentApiFile() { return "thirdParty.api"; } /** * Converts the auth config from the auth annotation. Subclasses may override * to add additional information to the auth config. */ private void convertApiAuth(ObjectNode root, ApiAuthConfig config) { ObjectNode authConfig = objectMapper.createObjectNode(); authConfig.put("allowCookieAuth", config.getAllowCookieAuth()); List<String> blockedRegions = config.getBlockedRegions(); if (!blockedRegions.isEmpty()) { ArrayNode blockedRegionsNode = objectMapper.createArrayNode(); for (String region : blockedRegions) { blockedRegionsNode.add(region); } authConfig.set("blockedRegions", blockedRegionsNode); } root.set("auth", authConfig); } private void convertApiFrontendLimits(ObjectNode root, ApiFrontendLimitsConfig config) { ObjectNode frontendLimitsConfig = objectMapper.createObjectNode(); frontendLimitsConfig.put("unregisteredUserQps", config.getUnregisteredUserQps()); frontendLimitsConfig.put("unregisteredQps", config.getUnregisteredQps()); frontendLimitsConfig.put("unregisteredDaily", config.getUnregisteredDaily()); convertApiFrontendLimitRules(frontendLimitsConfig, config.getRules()); root.set("frontendLimits", frontendLimitsConfig); } private void convertApiCacheControl(ObjectNode root, ApiCacheControlConfig config) { ObjectNode cacheControlConfig = objectMapper.createObjectNode(); cacheControlConfig.put("type", config.getType()); cacheControlConfig.put("maxAge", config.getMaxAge()); root.set("cacheControl", cacheControlConfig); } private void convertApiNamespace(ObjectNode root, ApiNamespaceConfig config) { if (!config.getOwnerDomain().isEmpty()) { root.put("ownerDomain", config.getOwnerDomain()); } if (!config.getOwnerName().isEmpty()) { root.put("ownerName", config.getOwnerName()); } if (!config.getPackagePath().isEmpty()) { root.put("packagePath", config.getPackagePath()); } } private void convertApiFrontendLimitRules(ObjectNode frontendLimitsConfig, List<ApiFrontendLimitsConfig.FrontendLimitsRule> rules) { ArrayNode rulesConfig = objectMapper.createArrayNode(); for (ApiFrontendLimitsConfig.FrontendLimitsRule rule : rules) { // TODO: Allow overriding individual rules based on same "match" field? ObjectNode ruleConfig = objectMapper.createObjectNode(); ruleConfig.put("match", rule.getMatch()); ruleConfig.put("qps", rule.getQps()); ruleConfig.put("userQps", rule.getUserQps()); ruleConfig.put("daily", rule.getDaily()); ruleConfig.put("analyticsId", rule.getAnalyticsId()); rulesConfig.add(ruleConfig); } frontendLimitsConfig.set("rules", rulesConfig); } private void convertApiMethods(Iterable<? extends ApiConfig> configs, ObjectNode root) throws IllegalArgumentException, SecurityException, ApiConfigException { ObjectNode methodsNode = objectMapper.createObjectNode(); ObjectNode descriptorNode = objectMapper.createObjectNode(); ObjectNode descriptorSchemasNode = objectMapper.createObjectNode(); ObjectNode descriptorMethodsNode = objectMapper.createObjectNode(); descriptorNode.set("schemas", descriptorSchemasNode); descriptorNode.set("methods", descriptorMethodsNode); for (ApiConfig config : configs) { convertApiMethods(methodsNode, descriptorSchemasNode, descriptorMethodsNode, config); } root.set("methods", methodsNode); root.set("descriptor", descriptorNode); } private void convertApiMethods(ObjectNode methodsNode, ObjectNode descriptorSchemasNode, ObjectNode descriptorMethodsNode, ApiConfig apiConfig) throws IllegalArgumentException, SecurityException, ApiConfigException { Map<EndpointMethod, ApiMethodConfig> methodConfigs = apiConfig.getApiClassConfig().getMethods(); for (Map.Entry<EndpointMethod, ApiMethodConfig> methodConfig : methodConfigs.entrySet()) { if (!methodConfig.getValue().isIgnored()) { EndpointMethod endpointMethod = methodConfig.getKey(); ApiMethodConfig config = methodConfig.getValue(); convertApiMethod(methodsNode, descriptorSchemasNode, descriptorMethodsNode, endpointMethod, config, apiConfig); } } } private void convertApiMethod(ObjectNode methodsNode, ObjectNode descriptorSchemasNode, ObjectNode descriptorMethodsNode, EndpointMethod endpointMethod, ApiMethodConfig config, ApiConfig apiConfig) throws IllegalArgumentException, SecurityException, ApiConfigException { ObjectNode methodNode = objectMapper.createObjectNode(); setNodePropertyNoConflict(methodsNode, config.getFullMethodName(), methodNode); methodNode.put("path", config.getPath()); methodNode.put("description", config.getDescription()); methodNode.put("httpMethod", config.getHttpMethod()); methodNode.set("authLevel", objectMapper.convertValue(config.getAuthLevel(), JsonNode.class)); methodNode.set("scopes", objectMapper.convertValue(AuthScopeExpressions.encode(config.getScopeExpression()), JsonNode.class)); methodNode.set("audiences", objectMapper.convertValue(config.getAudiences(), JsonNode.class)); methodNode.set("clientIds", objectMapper.convertValue(config.getClientIds(), JsonNode.class)); methodNode.put("rosyMethod", config.getFullJavaName()); ObjectNode descriptorMethodNode = objectMapper.createObjectNode(); setNodePropertyNoConflict(descriptorMethodsNode, config.getFullJavaName(), descriptorMethodNode); convertMethodRequest(endpointMethod, methodNode, descriptorSchemasNode, descriptorMethodNode, config, apiConfig); convertMethodResponse(endpointMethod, methodNode, descriptorSchemasNode, descriptorMethodNode, config); } private void convertMethodRequest(EndpointMethod endpointMethod, ObjectNode apiMethodNode, ObjectNode descriptorSchemasNode, ObjectNode descriptorMethodNode, ApiMethodConfig config, ApiConfig apiConfig) throws IllegalArgumentException, SecurityException, ApiConfigException { ObjectNode requestNode = objectMapper.createObjectNode(); convertMethodRequestParameters(endpointMethod, requestNode, descriptorSchemasNode, descriptorMethodNode, config, apiConfig); apiMethodNode.set("request", requestNode); } private void convertMethodRequestParameters(EndpointMethod endpointMethod, ObjectNode requestNode, ObjectNode descriptorSchemasNode, ObjectNode descriptorMethodNode, ApiMethodConfig config, ApiConfig apiConfig) throws IllegalArgumentException, SecurityException, ApiConfigException { ObjectNode parametersNode = objectMapper.createObjectNode(); Method method = endpointMethod.getMethod(); List<ApiParameterConfig> parameterConfigs = config.getParameterConfigs(); for (ApiParameterConfig parameterConfig : parameterConfigs) { switch (parameterConfig.getClassification()) { case INJECTED: // Do nothing. break; case API_PARAMETER: convertSimpleParameter(parameterConfig, parametersNode); break; case RESOURCE: // Inserts resource in. convertComplexParameter(parameterConfig, method, descriptorSchemasNode, descriptorMethodNode, apiConfig, parameterConfigs); break; case UNKNOWN: throw new IllegalArgumentException("Unclassifiable parameter type found."); } } // Set API parameter types if needed. if (parametersNode.size() != 0) { requestNode.set("parameters", parametersNode); } // Sets request body to auto-template if Lily request portion is set.. if (descriptorMethodNode.get("request") != null) { requestNode.put("body", "autoTemplate(backendRequest)"); requestNode.put("bodyName", "resource"); } else { requestNode.put("body", "empty"); } } /* * This appends to the API file any type which can go into the methods.request.parameters * portion of the resulting .api file. This includes parameter types and any type, such as * enum or lists of simple types, which can be converted into simple types. */ private void convertSimpleParameter(ApiParameterConfig config, ObjectNode parametersNode) { ObjectNode parameterNode = objectMapper.createObjectNode(); TypeToken<?> type; if (config.isRepeated()) { parameterNode.put("repeated", true); type = config.getRepeatedItemSerializedType(); } else { type = config.getSchemaBaseType(); } if (config.isEnum()) { ObjectNode enumValuesNode = objectMapper.createObjectNode(); for (Object enumConstant : type.getRawType().getEnumConstants()) { ObjectNode enumNode = objectMapper.createObjectNode(); enumValuesNode.set(enumConstant.toString(), enumNode); } parameterNode.set("enum", enumValuesNode); type = TypeToken.of(String.class); } parameterNode.put("type", typeLoader.getParameterTypes().get(type.getRawType())); parameterNode.put("description", config.getDescription()); parameterNode.put("required", !config.getNullable() && config.getDefaultValue() == null); // TODO: Try to find a way to move default value interpretation/conversion into the // general configuration code. String defaultValue = config.getDefaultValue(); if (defaultValue != null) { Class<?> parameterClass = type.getRawType(); try { objectMapper.convertValue(defaultValue, parameterClass); } catch (IllegalArgumentException e) { throw new IllegalArgumentException( String.format("'%s' is not a valid default value for type '%s'", defaultValue, type)); } parameterNode.put("default", defaultValue); } parametersNode.set(config.getName(), parameterNode); } /* * This appends to the API file any type which can NOT go into the methods.request.parameters. * These will be present as Json schemas inside the Lily descriptor portion of the API file. */ private void convertComplexParameter(ApiParameterConfig config, Method method, ObjectNode descriptorSchemasNode, ObjectNode descriptorMethodNode, ApiConfig apiConfig, List<ApiParameterConfig> parameterConfigs) throws ApiConfigException { TypeToken<?> type = config.getSchemaBaseType(); ObjectNode requestTypeNode = objectMapper.createObjectNode(); addTypeToNode(descriptorSchemasNode, type, null, requestTypeNode, apiConfig, parameterConfigs); setNodePropertyNoConflict(descriptorMethodNode, "request", requestTypeNode, "Method " + method.getDeclaringClass().getName() + "." + method.getName() + " cannot have multiple resource parameters"); } private void convertMethodResponse(EndpointMethod serviceMethod, ObjectNode methodNode, ObjectNode descriptorSchemasNode, ObjectNode descriptorMethodNode, ApiMethodConfig config) throws ApiConfigException { ObjectNode responseNode = objectMapper.createObjectNode(); methodNode.set("response", responseNode); if (config.hasResourceInResponse()) { responseNode.put("body", "autoTemplate(backendResponse)"); // TODO: Get from ApiMethodConfig. TypeToken<?> returnType = ApiAnnotationIntrospector.getSchemaType(serviceMethod.getReturnType(), config.getApiClassConfig().getApiConfig()); descriptorMethodNode.set("response", convertMethodResponseType(descriptorSchemasNode, returnType, config)); } else { // Void methods don't generate response sections in the descriptor responseNode.put("body", "empty"); } } /** * Returns a node with the response object type, wrapping any arrays into a new Collection schema. */ private ObjectNode convertMethodResponseType(ObjectNode descriptorSchemasNode, TypeToken<?> returnType, ApiMethodConfig config) throws ApiConfigException { ObjectNode returnTypeNode = objectMapper.createObjectNode(); String responseTypeName = addTypeToNode(descriptorSchemasNode, returnType, null, returnTypeNode, config.getApiClassConfig().getApiConfig(), null); if (Types.isArrayType(returnType)) { ObjectNode propertiesNode = objectMapper.createObjectNode(); propertiesNode.set("items", returnTypeNode); ObjectNode arrayWrapperNode = objectMapper.createObjectNode(); arrayWrapperNode.put("id", responseTypeName); arrayWrapperNode.put("type", "object"); arrayWrapperNode.set("properties", propertiesNode); descriptorSchemasNode.set(responseTypeName, arrayWrapperNode); returnTypeNode = objectMapper.createObjectNode(); returnTypeNode.put("$ref", responseTypeName); } return returnTypeNode; } @VisibleForTesting String addTypeToSchema(ObjectNode schemasNode, TypeToken<?> type, ApiConfig apiConfig, List<ApiParameterConfig> parameterConfigs) throws ApiConfigException { return addTypeToSchema(schemasNode, type, null, apiConfig, parameterConfigs); } /** * Adds an arbitrary, non-array type to a given schema config. * * @param schemasNode the config to store the generated type schema * @param type the type from which to generate a schema * @param enclosingType for bean properties, the enclosing bean type, used for resolving type * variables * @return the name of the schema generated from the type */ @VisibleForTesting String addTypeToSchema(ObjectNode schemasNode, TypeToken<?> type, TypeToken<?> enclosingType, ApiConfig apiConfig, List<ApiParameterConfig> parameterConfigs) throws ApiConfigException { if (typeLoader.isSchemaType(type)) { return typeLoader.getSchemaType(type); } else if (Types.isObject(type)) { if (!schemasNode.has(ANY_SCHEMA_NAME)) { ObjectNode anySchema = objectMapper.createObjectNode(); anySchema.put("id", ANY_SCHEMA_NAME); anySchema.put("type", "any"); schemasNode.set(ANY_SCHEMA_NAME, anySchema); } return ANY_SCHEMA_NAME; } else if (Types.isMapType(type)) { if (!schemasNode.has(MAP_SCHEMA_NAME)) { ObjectNode mapSchema = objectMapper.createObjectNode(); mapSchema.put("id", MAP_SCHEMA_NAME); mapSchema.put("type", "object"); schemasNode.set(MAP_SCHEMA_NAME, mapSchema); } return MAP_SCHEMA_NAME; } // If we already have this schema defined, don't define it again! String typeName = Types.getSimpleName(type, apiConfig.getSerializationConfig()); JsonNode existing = schemasNode.get(typeName); if (existing != null && existing.isObject()) { return typeName; } ObjectNode schemaNode = objectMapper.createObjectNode(); Class<?> c = type.getRawType(); if (c.isEnum()) { schemasNode.set(typeName, schemaNode); schemaNode.put("id", typeName); schemaNode.put("type", "string"); ArrayNode enumNode = objectMapper.createArrayNode(); for (Object enumConstant : c.getEnumConstants()) { enumNode.add(enumConstant.toString()); } schemaNode.set("enum", enumNode); } else { // JavaBean TypeToken<?> serializedType = ApiAnnotationIntrospector.getSchemaType(type, apiConfig); if (!type.equals(serializedType)) { return addTypeToSchema(schemasNode, serializedType, enclosingType, apiConfig, parameterConfigs); } else { addBeanTypeToSchema(schemasNode, typeName, schemaNode, type, apiConfig, parameterConfigs); } } return typeName; } private void addBeanTypeToSchema(ObjectNode schemasNode, String typeName, ObjectNode schemaNode, TypeToken<?> type, ApiConfig apiConfig, List<ApiParameterConfig> parameterConfigs) throws ApiConfigException { schemasNode.set(typeName, schemaNode); schemaNode.put("id", typeName); schemaNode.put("type", "object"); ObjectNode propertiesNode = objectMapper.createObjectNode(); addBeanProperties(schemasNode, propertiesNode, type, apiConfig, parameterConfigs); schemaNode.set("properties", propertiesNode); } /** * Iterates over the given JavaBean class and adds the following to the given config object * (the value of "properties" of a schema object): "<name>": {"type": "<type>"}, where * "name" is the name of the JavaBean property and "type" the type of its value. */ private void addBeanProperties(ObjectNode schemasNode, ObjectNode node, TypeToken<?> beanType, ApiConfig apiConfig, List<ApiParameterConfig> parameterConfigs) throws ApiConfigException { // CollectionResponse<T> is treated as a bean but it is a parameterized type, too. ResourceSchema schema = resourceSchemaProvider.getResourceSchema(beanType, apiConfig); for (Entry<String, ResourcePropertySchema> entry : schema.getProperties().entrySet()) { String propertyName = entry.getKey(); ObjectNode propertyNode = objectMapper.createObjectNode(); TypeToken<?> propertyType = entry.getValue().getType(); if (propertyType != null) { addTypeToNode(schemasNode, propertyType, beanType, propertyNode, apiConfig, parameterConfigs); node.set(propertyName, propertyNode); } } } /** * Adds a schema for a type into an output node. For arrays, this generates nested schemas inline * for however many dimensions are necessary. * * @return an appropriate name for the schema if one isn't already assigned */ private String addTypeToNode(ObjectNode schemasNode, TypeToken<?> type, TypeToken<?> enclosingType, ObjectNode node, ApiConfig apiConfig, List<ApiParameterConfig> parameterConfigs) throws ApiConfigException { TypeToken<?> itemType = Types.getArrayItemType(type); if (typeLoader.isSchemaType(type)) { String basicTypeName = typeLoader.getSchemaType(type); addElementTypeToNode(schemasNode, type, basicTypeName, node, apiConfig); return basicTypeName; } else if (itemType != null) { ObjectNode items = objectMapper.createObjectNode(); node.put("type", "array"); node.set(Constant.ITEMS, items); String itemTypeName = addTypeToNode(schemasNode, itemType, enclosingType, items, apiConfig, parameterConfigs); String arraySuffix = "Collection"; StringBuilder sb = new StringBuilder(itemTypeName.length() + arraySuffix.length()); sb.append(itemTypeName).append(arraySuffix); sb.setCharAt(0, Character.toUpperCase(sb.charAt(0))); return sb.toString(); } else if (type instanceof TypeVariable) { throw new IllegalArgumentException(String.format("Object type %s not supported.", type)); } else { String typeName = addTypeToSchema(schemasNode, type, enclosingType, apiConfig, parameterConfigs); addElementTypeToNode(schemasNode, type, typeName, node, apiConfig); return typeName; } } /** * Adds a basic (non-array) type to an output node, assuming the type has a corresponding schema * in the provided configuration if it will ever have one. */ private void addElementTypeToNode(ObjectNode schemasNode, TypeToken<?> type, String typeName, ObjectNode node, ApiConfig apiConfig) { // This check works better than checking schemaTypes in the case of Map<K, V> if (schemasNode.has(typeName)) { node.put("$ref", typeName); } else { node.put("type", typeName); String format = schemaFormatForType(type, apiConfig); if (format != null) { node.put("format", format); } } } // If a type has a serializer installed, resolve down to target type of the serialization chain to // find the schema format. @Nullable private String schemaFormatForType(TypeToken<?> type, ApiConfig apiConfig) { TypeToken<?> serializedType = ApiAnnotationIntrospector.getSchemaType(type, apiConfig); if (!type.equals(serializedType)) { return schemaFormatForType(serializedType, apiConfig); } return typeLoader.getSchemaFormat(type); } }