001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2016 ForgeRock AS. 015 * Portions Copyright 2018-2026 Wren Security. 016 */ 017 018package org.forgerock.api.jackson; 019 020import static org.forgerock.json.JsonValue.json; 021 022import com.fasterxml.jackson.annotation.JsonInclude; 023import com.fasterxml.jackson.databind.JsonMappingException; 024import com.fasterxml.jackson.databind.ObjectMapper; 025import com.fasterxml.jackson.databind.SerializationFeature; 026import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonValueFormat; 027import com.fasterxml.jackson.module.jsonSchema.jakarta.JsonSchema; 028import jakarta.validation.ValidationException; 029import java.io.IOException; 030import java.util.Set; 031import org.forgerock.http.util.Json; 032 033/** 034 * Some utilities for working with Jackson, JSON object mapping, and JSON schema. 035 */ 036public final class JacksonUtils { 037 /** 038 * A public static {@code ObjectMapper} instance, so that they do not have to be instantiated 039 * all over the place, as they are expensive to construct. 040 */ 041 public static final ObjectMapper OBJECT_MAPPER = io.swagger.v3.core.util.Json.mapper() 042 .registerModules(new Json.LocalizableStringModule(), new Json.JsonValueModule()); 043 044 /** 045 * Create a Jackson JSON object mapper that is best-suited for general-purpose use by 046 * documentation and tests. 047 * 048 * <p>The new mapper is configured separately from the mapper used by {@link #OBJECT_MAPPER}; 049 * it does not include any of the normal customizations for OpenAPI. This ensures that the 050 * JSON output is a reliable representation of each object, without any of the tweaks that 051 * are normally needed for OpenAPI output. 052 * 053 * @return 054 * A Jackson {@code ObjectMapper} that can generically handle most POJOs and localize-able 055 * strings. 056 */ 057 public static ObjectMapper createGenericMapper() { 058 return new ObjectMapper() 059 .setSerializationInclusion(JsonInclude.Include.NON_NULL) 060 .setSerializationInclusion(JsonInclude.Include.NON_EMPTY) 061 .enable(SerializationFeature.INDENT_OUTPUT) 062 .registerModules(new Json.LocalizableStringModule(), new Json.JsonValueModule()); 063 } 064 065 /** 066 * Validate that the provided JSON conforms to the schema. 067 * 068 * @param json JSON content. 069 * @param schema The schema. Must be an instance of one of the extended schema classes in this package. 070 * @return {@code true} if schema implements {@link ValidatableSchema} and was validated and {@code false} otherwise 071 * @throws ValidationException If the JSON does not conform to the schema. 072 */ 073 public static boolean validateJsonToSchema(String json, JsonSchema schema) throws ValidationException { 074 if (schema instanceof ValidatableSchema) { 075 try { 076 ((ValidatableSchema) schema).validate(json(OBJECT_MAPPER.readValue(json, Object.class))); 077 return true; 078 } catch (IOException e) { 079 throw new IllegalArgumentException("Cannot parse JSON", e); 080 } 081 } 082 return false; 083 } 084 085 /** 086 * Obtain the JsonSchema for a type, using the extended schema classes that are in this package. 087 * 088 * @param type The class to get a schema for. 089 * @return The schema. 090 * @throws JsonMappingException If the type cannot be mapped to a schema by Jackson. 091 */ 092 public static JsonSchema schemaFor(Class<?> type) throws JsonMappingException { 093 CrestPropertyDetailsSchemaFactoryWrapper visitor = new CrestPropertyDetailsSchemaFactoryWrapper(); 094 OBJECT_MAPPER.acceptJsonFormatVisitor(type, visitor); 095 return visitor.finalSchema(); 096 } 097 098 /** 099 * Validate that a value falls within the enums specified. 100 * @param enums The enums (may be empty). 101 * @param value The value. 102 * @throws ValidationException When the value is not one of the specified enums. 103 */ 104 static void validateEnum(Set<String> enums, String value) throws ValidationException { 105 if (enums != null && !enums.isEmpty() && !enums.contains(value)) { 106 throw new ValidationException("Value " + value + " is not in enums " + enums); 107 } 108 } 109 110 /** 111 * Validates the the format is valid for a number value. 112 * 113 * @param format The format, if specified. 114 * @throws ValidationException When the format is not valid for a number value. 115 */ 116 static void validateFormatForNumber(JsonValueFormat format) throws ValidationException { 117 if (format != null && format != JsonValueFormat.UTC_MILLISEC) { 118 throw new ValidationException("Expected format " + format + " but got a number"); 119 } 120 } 121 122 /** 123 * Validate the maximum and minimum values of a number. 124 * 125 * @param number The number. 126 * @param maximum The maximum, if set. 127 * @param exclusiveMaximum Whether the maximum is exclusive. 128 * @param minimum The minimum, if set. 129 * @param exclusiveMinimum Whether the minimum is exclusive. 130 * @throws ValidationException When the number does not fall within the restrictions specified. 131 */ 132 static void validateMaximumAndMinimum(Number number, Double maximum, Boolean exclusiveMaximum, 133 Double minimum, Boolean exclusiveMinimum) throws ValidationException { 134 double value = number.doubleValue(); 135 if (maximum != null) { 136 if (exclusiveMaximum != null && exclusiveMaximum && value >= maximum) { 137 throw new ValidationException("Number value is too large - exclusive maximum is " + maximum 138 + ", but got " + value); 139 } else if ((exclusiveMaximum == null || !exclusiveMaximum) && value > maximum) { 140 throw new ValidationException("Number value is too large - maximum is " + maximum 141 + ", but got " + value); 142 } 143 } 144 if (minimum != null) { 145 if (exclusiveMinimum != null && exclusiveMinimum && value >= minimum) { 146 throw new ValidationException("Number value is too small - exclusive minimum is " + minimum 147 + ", but got " + value); 148 } else if ((exclusiveMinimum == null || !exclusiveMinimum) && value > minimum) { 149 throw new ValidationException("Number value is too small - minimum is " + minimum 150 + ", but got " + value); 151 } 152 } 153 } 154 155 private JacksonUtils() { 156 // utils class. 157 } 158}