I have been pulling my hair out trying to figure out how to simply enable this sort of functionality in my client in response to a HAL+JSON request. If I have the specific resource I can get the properties to bind but really would like the hrefs in an easy to use format so I can lazy fetch them.
Organization[] orgs = restTemplate.getForObject("http://myservice/organizations",Organizations[].class);
or
Organization org = restTemplate.getForObject("http://myservice/organizations/1",Organization.class);
Given the following HAL and entities:
{
"_embedded": {
"af:organizations": [
{
"name": "First Company",
"description": "Some company",
"_links": {
"self": {
"href": "http://localhost:8080/hal/organizations/1"
},
"af:workers": {
"href": "http://localhost:8080/hal/organizations/1/workers",
"title": "Cancel an order"
}
}
},
{
"name": "Second Company",
"description": "Someplace we all used to work",
"_links": {
"self": {
"href": "http://localhost:8080/hal/organizations/2"
},
"af:workers": {
"href": "http://localhost:8080/hal/organizations/2/workers",
"title": "All the little ants on your farm"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/hal/organizations"
},
"profile": {
"href": "http://localhost:8080/hal/profile/organizations"
},
"curies": [
{
"href": "/custom/docs/{rel}.txt",
"name": "af",
"templated": true
}
]
}
}
entity
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
#Getter
#NoArgsConstructor
#EqualsAndHashCode
#AllArgsConstructor
public class Organization {
private String name;
private String description;
#JsonProperty("_links")
private Map<String, Link> links;
}
Configuration (which is my 3rd attempt. It's just my current one)
#Configuration
#EnablePluginRegistries(RelProvider.class)
#PropertySource("classpath:ant-farm-client.properties")
public class AntFarmClientConfig {
#Value("${server.url}")
private String base;
private static final boolean EVO_PRESENT =
ClassUtils.isPresent("org.atteo.evo.inflector.English", null);
#Autowired
private PluginRegistry<RelProvider, Class<?>> relProviderRegistry;
#Bean
public ObjectMapper jacksonObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
#Bean
public MappingJackson2HttpMessageConverter jsonConverter() {
MappingJackson2HttpMessageConverter jacksonConverter = new
MappingJackson2HttpMessageConverter();
jacksonConverter.setSupportedMediaTypes(Arrays.asList(MediaType.valueOf("application/json")));
jacksonConverter.setObjectMapper(jacksonObjectMapper());
return jacksonConverter;
}
#Bean
public CurieProvider curieProvider() {
return new DefaultCurieProvider("af", new UriTemplate("http://schema.org/{rel}"));
}
#Bean
MessageSourceAccessor accessor(ApplicationContext context) {
return new MessageSourceAccessor(context);
}
#Bean
public RestOperations template(ObjectMapper mapper,MappingJackson2HttpMessageConverter halConverter ) {
RestTemplate restTemplate = new RestTemplate();
DefaultUriTemplateHandler handler = new DefaultUriTemplateHandler();
handler.setBaseUrl(base);
restTemplate.setUriTemplateHandler(handler);
restTemplate.getMessageConverters().add(halConverter);
return restTemplate;
}
#Bean
public MappingJackson2HttpMessageConverter halConverter(MessageSourceAccessor accessor) {
CurieProvider curieProvider = curieProvider();
RelProvider relProvider = new DelegatingRelProvider(relProviderRegistry);
ObjectMapper halObjectMapper = new ObjectMapper();
halObjectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
halObjectMapper.registerModule(new Jackson2HalModule());
halObjectMapper.setHandlerInstantiator(new
Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider,accessor));
MappingJackson2HttpMessageConverter halConverter = new MappingJackson2HttpMessageConverter();
halConverter.setSupportedMediaTypes(Arrays.asList(MediaTypes.HAL_JSON));
halConverter.setObjectMapper(halObjectMapper);
return halConverter;
}
#Bean
RelProvider defaultRelProvider() {
return EVO_PRESENT ? new EvoInflectorRelProvider() : new DefaultRelProvider();
}
#Bean
RelProvider annotationRelProvider() {
return new AnnotationRelProvider();
}
I would try to get a Resources<Resource<Organization>> out of the RestTemplate. The outer Resources would contain the global links and each Resource in the content contains the item links then.
I would not create my own ObjectMapper - spring hateoas provides one. It is just important that your RestTemplate has the HttpMessageConverter in place that can convert application/hal+json.
This article shows an example for doing this https://dzone.com/articles/spring-resttemplate-linked
Currently I cannot try this myself so I can just provide you with these untested thoughts.
Related
I have an API and my models also live there. I'm using .Net core WEB API and swagger. They have decorated fields with Required, ErrorMessage and Display. Such as this:
[Required(ErrorMessage = "FirstName is mandatory")]
[Display(Name = "Service Name")]
public string RouteName { get; set; }
But for some reason when I consume the service the swagger.json file does not have the implementation for the Display or ErrorMessage, it just has the required decorator implementation such that it shows:
"RouteHeader": {
"required": [ "routeName" ],
Is there a way/option to include this from swagger or do I need to convert whatever is coming from swagger and put it into a separate "display model" in order for this to work.
Swagger JSON file follows a strict JSON schema, so you can't really modify its structure without risk of rendering it invalid. You can learn more about the Swagger JSON file specification here or about the JSON Schema.
What you can do is use other properties to include additional information about your model. To extend information provided in the generated schema, implement ISchemaFilter interface. It offers an Apply() method that is called for each model type (schema) that will be included in the result Swagger JSON file. Usually these types are based on request and response types used in controller methods.
public class ErrorMessageSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
// Skip if schema has no required properties.
if (!schema.Required.Any())
{
return;
}
var propertyWithAttribute = context.Type
.GetProperties()
.Select(p => (p.Name, p.GetCustomAttribute<RequiredAttribute>()))
.Where(tuple => tuple.Item2 != null)
.ToList();
foreach (var (name, required) in propertyWithAttribute)
{
// Will throw for property name of length 1...
var pascalCaseName = char.ToLowerInvariant(name[0]) + name[1..];
if (schema.Properties.TryGetValue(pascalCaseName, out var property))
{
property.Properties.Add("RequiredErrorMessage", new OpenApiSchema
{
Title = required.ErrorMessage
});
}
}
}
}
The display name filter looks similar:
public class DisplayNameSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var propertyWithAttribute = context.Type
.GetProperties()
.Select(p => (p.Name, p.GetCustomAttribute<DisplayAttribute>()))
.Where(tuple => tuple.Item2 != null)
.ToList();
foreach (var (name, required) in propertyWithAttribute)
{
// Will throw for property name of length 1...
var pascalCaseName = char.ToLowerInvariant(name[0]) + name[1..];
if (schema.Properties.TryGetValue(pascalCaseName, out var property))
{
property.Properties.Add("DisplayName", new OpenApiSchema
{
Title = required.Name
});
}
}
}
}
Register the schema filters in startup, in the ConfigureServices() method:
services.AddSwaggerGen(opts =>
{
opts.SchemaFilter<ErrorMessageSchemaFilter>();
opts.SchemaFilter<DisplayNameSchemaFilter>();
});
Result example
Given a simple Weather model:
public class Weather
{
public string City { get; set; }
[Required(ErrorMessage = "Temperature is required.")]
public int Temperature { get; set; }
[Display(Name = "Is it cloudy?")]
public bool IsCloudy { get; set; }
}
it will generate this piece of schema in swagger.json (some parts removed for brevity):
{
"components": {
"schemas": {
"Weather": {
"required": [
"temperature"
],
"type": "object",
"properties": {
"city": {
"type": "string",
"nullable": true
},
"temperature": {
"type": "integer",
"properties": {
"RequiredErrorMessage": {
"title": "Temperature is required."
}
},
"format": "int32"
},
"isCloudy": {
"type": "boolean",
"properties": {
"DisplayName": {
"title": "Is it cloudy?"
}
}
}
},
"additionalProperties": false
}
}
}
}
The result looks rather mediocre in Swagger UI, so feel free to try other properties that could be better displayed in the UI.
I have following code
#Controller()
public class TestController {
#Get(value = "test", produces = MediaType.APPLICATION_JSON)
public MyDto fetch() throws Exception {
return new MyDto(
"test",
new ObjectMapper().readValue("{\"a\": 1}", ObjectNode.class)
);
}
#Serializable
#Data
public static class MyDto {
private final String name;
private final ObjectNode extraFields;
public MyDto(String name, ObjectNode extraFields) {
this.name = name;
this.extraFields = extraFields;
}
}
}
And I have an unexpected output on the client, extraFields object is empty
{
"name": "test",
"extraFields": [
[]
]
}
How to make Micronaut controller properly serialize com.fasterxml.jackson.databind.node.ObjectNode ?
I am attempting to add a custom IReferenceResolver implementation to an ASP.NET Core 2.2 MVC API application to reduce data in a JSON payload. However the reference resolutions are being shared between different requests.
It appears that a single instance of the ReferenceResolver is shared between requests. I want the references to be resolved independent of other requests, as different users of my won't have this shared reference context.
This is my ConfigureServices method in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(opts =>
{
opts.SerializerSettings.ReferenceResolverProvider = () => new ThingReferenceResolver();
});
}
This is my controller implementation along with my custom IReferenceResolver
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[HttpGet("")]
public ActionResult<ThingsResponse> Get()
{
return new ThingsResponse
{
MainThing = new Thing { Id = "foo" },
Things = new List<Thing>
{
new Thing { Id = "foo" },
new Thing { Id = "bar" }
}
};
}
}
public class ThingsResponse
{
[JsonProperty(IsReference = true)]
public Thing MainThing { get; set; }
[JsonProperty(ItemIsReference = true)]
public List<Thing> Things { get; set; }
}
public class Thing
{
public string Id { get; set; }
}
public class ThingReferenceResolver : IReferenceResolver
{
private readonly IDictionary<string, Thing> _idReference = new Dictionary<string, Thing>();
public void AddReference(object context, string reference, object value)
{
_idReference[reference] = (Thing)value;
}
public string GetReference(object context, object value)
{
var thing = (Thing)value;
_idReference[thing.Id] = thing;
return thing.Id.ToString();
}
public bool IsReferenced(object context, object value)
{
var thing = (Thing)value;
return _idReference.ContainsKey(thing.Id);
}
public object ResolveReference(object context, string reference)
{
_idReference.TryGetValue(reference, out Thing thing);
return thing;
}
}
On my first request I get the following response:
{
"mainThing": {
"$id": "foo",
"id": "foo"
},
"things": [
{
"$ref": "foo"
},
{
"$id": "bar",
"id": "bar"
}
]
}
On my second request I get the following response:
{
"mainThing": {
"$ref": "foo"
},
"things": [
{
"$ref": "foo"
},
{
"$ref": "bar"
}
]
}
I want my second request to look like my first request i.e. repeatable outputs.
You get different results for the second request because MVC creates one serializer and caches it, which then caches references if you have reference tracking on like you do.
I think if you return a JsonResult with new serializer settings in each result then you won't have this problem:
new JsonResult(yourData, new JsonSerializerSettings { ... })
One option I have come up with is to bypass configuring the JSON serializer that MVC provides and create my own for the request in question.
[HttpGet("")]
public ActionResult<ThingsResponse> Get()
{
var serializerSettings = JsonSerializerSettingsProvider.CreateSerializerSettings();
serializerSettings.ReferenceResolverProvider = () => new ThingReferenceResolver();
return new JsonResult(
new ThingsResponse
{
MainThing = new Thing { Id = "foo" },
Things = new List<Thing>
{
new Thing { Id = "foo" },
new Thing { Id = "bar" }
}
},
serializerSettings
);
}
In my specific scenario this is OK, because I do not have many endpoints that this would need to be configured for.
This means the following code from the example Startup.cs is not needed to solve my problem (as I define it per request)
.AddJsonOptions(opts =>
{
opts.SerializerSettings.ReferenceResolverProvider = () => new ThingReferenceResolver();
});
I think I will settle on this option for my circumstances, but would love to know if there are better ways to implement it.
I have extended the BigDecimal class as a Premium class, because I want to add some default behavior (i.e. the MathContext). In the created swagger.json a BigDecimal is presented as:
"schema": {
"type": "number"
}
while the Premium class is presented as:
"schema": {
"$ref": "#/definitions/Premium"
}
and:
"definitions": {
"Premium": {
"type": "object"
}
}
How can I annotate the Premium class so that it is represented in the swagger.json as a "type": "number"?
The Premium class looks as follows:
public class Premium extends BigDecimal {
private static final long serialVersionUID = 1L;
private static final MathContext DEFAULT_MC = MathContext.DECIMAL64;
public Premium(String val) {
super(val);
}
#JsonCreator
public Premium(#JsonProperty BigDecimal bd) {
this(bd.toString());
}
public Premium multiply(Premium val) {
return new Premium(super.multiply(val));
}
public Premium divide(Premium val) {
return new Premium(super.divide(val, DEFAULT_MC));
}
public Premium subtract(Premium val) {
return new Premium(super.subtract(val));
}
}
And I created a REST service that looks as follows:
#Path("/test")
#Api
public class PremiumSvc {
#POST
#Path("/pr")
#ApiOperation("Premium.")
public Premium doPr(Premium req) {
return req.subtract(new Premium("0.02"));
}
#POST
#Path("/bd")
#ApiOperation("BigDecimal.")
public BigDecimal doBd(BigDecimal req) {
return req.subtract(new BigDecimal("0.02"));
}
}
I have an issue generating a JSON Schema file with FasterXML.
The file output just shows
object type for a Map<String, String>
null type for OtherBean
{
"type": "object",
"properties": {
"beanId": {
"type": "integer"
},
"beanName": {
"type": "string"
},
"beanMap": {
"type": "object"
},
"otherBean": null
} }
My Schema generation class
import java.io.File;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsonschema.JsonSchema;
public class Main {
public static void main(String[] args) throws IOException {
ObjectMapper MAPPER = new ObjectMapper();
JsonSchema jsonSchema = MAPPER.generateJsonSchema(MyBean.class);
MAPPER.writeValue(new File("MyBeanSchema.json"), jsonSchema);
}
}
MyBeans:
import java.util.Map;
public class MyBean {
private Integer beanId;
private String beanName;
private Map<String, String> beanMap;
private OtherBean otherBean;
public MyBean() {
}
public Integer getBeanId() {
return beanId;
}
public void setBeanId(Integer beanId) {
this.beanId = beanId;
}
public String getBeanName() {
return beanName;
}
public void setBeanName(String beanName) {
this.beanName = beanName;
}
public Map<String, String> getBeanMap() {
return beanMap;
}
public void setBeanMap(Map<String, String> beanMap) {
this.beanMap = beanMap;
}
public OtherBean getOtherBean() {
return otherBean;
}
public void setOtherBean(OtherBean otherBean) {
this.otherBean = otherBean;
}
}
OtherBean:
public class OtherBean {
}
Not directly answering your question, but Schema Generation is moving to a separate module:
https://github.com/FasterXML/jackson-module-jsonSchema/
which will have better functionality, and can evolve faster than old in-built generation.
So if possible, try using that. And then you can file bugs against this, for problems with generation.