Generating swagger.json files with Enunciate and custom objects mappers

That’s a long title.
To make short, let’s just say I wanted to generate a Swagger file for Roboconf‘s REST API. This API is implemented in Java with Jersey and uses Jackson to handle the mapping between Java and JSon.

After exploring many solutions, I chose Enunciate to generate this file from my API. One of the best aspects of Enunciate is that it uses Javadoc comments (and optionally annotations) to populate the swagger.json file. The generated file can directly be read by Swagger UI.

Custom JSon de/serialization

One of the issues I met was related to our Java-JSon binding.
Indeed, our project uses custom object mappers. These mappers are a way for Jackson to tailor the JSon serialization and deserialization from and to Java objects. It is very convenient as it gives a maximum of control over what is returned by Jersey. However, this resulted in troubles when looking at the type definitions in the swagger.json file. Indeed, be it with Enunciate or Swagger Java tools, they all use Java types introspection to deduce the shape of the JSon objects. That’s a real problem if you use your own object mapper.

Fortunately, since version 2.6, Enunciate now supports Jackson mix-ins. A Jackson mix-in is in fact a Java class that you can annotate to customize the shape of the generated JSon structures. Let’s take an example. Let’s assume you have a model class that is returned by one of your REST operations.

public class MyModel {
     private String firstName, lastName;
     private int age;

     // All the setters and getters here...
}

Let’s assume this class is defined by someone else, in a different project. How can you customize the JSon generation as you cannot (or do not want) to modify the source code? Mix-ins to the rescue! You define another class and annotate it.

public abstract class MyModelMixin {

     // Do not serialize
     @JsonIgnore
     public abstract int getAge();

     // Change the property name
     @JsonProperty( "name" )
     public abstract String getLastName();
}

Jackson provides a way to associate these two classes in an object mapper. And since version 2.6, Enunciate also provides a way to define mix-ins through the enunciate.xml file (the file that configures what Enunciate generates).

<?xml version="1.0"?>
<enunciate 
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:noNamespaceSchemaLocation="http://enunciate.webcohesion.com/schemas/enunciate-2.6.0.xsd">

	<title>Roboconf REST API</title>
	<description>The REST API for Roboconf's Administration</description>
	<contact name="the Roboconf team" url="http://roboconf.net" />

	<modules>
		<!-- Disabled modules: almost all -->
		<jackson1 disabled="true" />
		<jaxb disabled="true" />
		<jaxws disabled="true" />
		<spring-web disabled="true" />
		<idl disabled="true" />

		<c-xml-client disabled="true" />
		<csharp-xml-client disabled="true" />
		<java-xml-client disabled="true" />
		<java-json-client disabled="true" />
		<gwt-json-overlay disabled="true" />
		<obj-c-xml-client disabled="true" />
		<php-xml-client disabled="true" />
		<php-json-client disabled="true" />
		<ruby-json-client disabled="true" />

		<!-- Enabled modules -->
		<jackson disabled="false" collapse-type-hierarchy="true">
			<mixin source="net.roboconf....MyModelMixin" target="net.roboconf....MyModel" />
		</jackson>
		<jaxrs disabled="false" />
		<docs disabled="false" />
		<swagger disabled="false" basePath="/roboconf-dm" host="localhost:8181" />
	</modules>

</enunciate>

You can define as many mixin elements that you need.

Pros and Cons

I tested this solution yesterday and it works fine.
I however found two drawbacks to it.

First, you have to write a second model just for the documentation. How can you verify that it is coherent with what you defined in your custom object mappers? The best option would be to get rid of your mapper and also rely on Jackson mixins for the Java-JSon conversions.

Second issue, Jackson mix-ins only work when you want to remove or update a JSon property. But they do not allow you to add properties. It is true in Enunciate but also in Jackson. There was a ticket created about it in the Jackson project but it was marked as “won’t be solved” as it would raise many problems.

Let’s illustrate it with our example.
Let’s imagine you defined in your object mapper a new JSon field called “fullName” which is the concatenation of both name parts. Updating your mixin with…

public abstract class MyModelMixin {

     // Do not serialize
     @JsonIgnore
     public abstract int getAge();

     // Change the property name
     @JsonProperty( "name" )
     public abstract String getLastName();

     // Add a new property (will not work)
     @JsonProperty( "fullName" )
     public abstract String getFullName();
}

… will not work.
You will not find a property called “fullName” in the swagger.json file. Mix-ins cannot address this use case. Maybe a new upgrade in Enunciate might do the trick for the documentation generation. But Jackson does not support it. You have to use custom object mappers.

So, to summer it up, Jackson mix-ins are to use only when you want to remove or rename JSon properties. Not to add new properties.

Workaround

There is no solution to directly mix custom objects mappers with Enunciate (or other Swagger tools). They all rely on a Java type hierarchies while custom object mappers let your creativity unbound.

So, you have to make a two-step process.
First, let Enunciate make most of the work, and then, fix what was generated. I implemented it with a Java class invoked during the project’s Maven build process through ANT. Let’s take a look at this. Here are the relevant parts of the POM file.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

	<!-- ... -->
	
	<properties>
		<enunciate.version>2.6.0</enunciate.version>
	</properties>
	
	<dependencies>
		
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>2.2.2</version>
			<scope>test</scope>
		</dependency>
	</dependencies>
	
	<build>
		<plugins>			
			<plugin>
				<groupId>com.webcohesion.enunciate</groupId>
				<artifactId>enunciate-maven-plugin</artifactId>
				<version>${enunciate.version}</version>
				<executions>
					<execution>
						<goals>
							<goal>docs</goal>
						</goals>
					</execution>
				</executions>
				<configuration>
					<docsDir>${project.build.directory}/docs</docsDir>
					<configFile>${project.basedir}/enunciate.xml</configFile>
				</configuration>
			</plugin>
			
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-antrun-plugin</artifactId>
				<executions>
					<execution>
						<phase>test-compile</phase>
						<configuration>
							<target>
								<property name="test_classpath" refid="maven.test.classpath"/>
								<java classname="net.roboconf.dm.rest.services.swagger.UpdateSwaggerJson" classpath="${test_classpath}" />
							</target>	
						</configuration>
						<goals>
							<goal>run</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			
			<plugin>
				<groupId>org.codehaus.mojo</groupId>
				<artifactId>build-helper-maven-plugin</artifactId>
				<version>1.12</version>
				<executions>
					<execution>
						<goals>
							<goal>attach-artifact</goal>
						</goals>
						<configuration>
							<artifacts>
								<artifact>
									<file>${project.build.directory}/docs/apidocs/ui/swagger.json</file>
									<type>json</type>
									<classifier>swagger</classifier>
								</artifact>
							</artifacts>
						</configuration>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

</project>

As you can see, there are 3 plug-in invocations in this sample. One is about the Enunciate Maven plug-in. It generates the documentation during the process-sources phase. The second one is the Maven ANT plug-in that executes a class located in the test sources. Indeed, since it is a build helper, there is no reason to put it in the main sources. This is why we perform this step during the test-compile phase. Notice we must pass the class path of the test sources. Eventually, we use the Maven build helper plug-in to attach the generated swagger.json file to the Maven artifacts. This way, this file will be deployed on our Maven repository along with our other built artifacts.

Let’s now take a look at our updater class.
It uses GSon to parse and update the JSon file.

public class UpdateSwaggerJson {

	final Set<Class<?>> processedClasses = new HashSet<> ();


	/**
	 * @param args
	 */
	public static void main( String[] args ) {

		try {
			UpdateSwaggerJson updater = new UpdateSwaggerJson();
			JsonObject newDef = updater.prepareNewDefinitions();
			updater.updateSwaggerJson( newDef );

		} catch( Exception e ) {
			e.printStackTrace();
		}
	}


	/**
	 * Prepares the JSon object to inject as the new definitions in the swagger.json file.
	 * @return a non-null object
	 * @throws IOException if something failed
	 */
	public JsonObject prepareNewDefinitions() throws IOException {

		ObjectMapper mapper = JSonBindingUtils.createObjectMapper();
		StringWriter writer = new StringWriter();
		JsonObject newDef = new JsonObject();

		// Create a model, as complete as possible
		TestApplication app = new TestApplication();
		app.bindWithApplication( "externalExportPrefix1", "application 1" );
		app.bindWithApplication( "externalExportPrefix1", "application 2" );
		app.bindWithApplication( "externalExportPrefix2", "application 3" );

		app.setName( "My Application with special chàràcters" );
		app.getTemplate().externalExports.put( "internalGraphVariable", "variableAlias" );
		app.getTemplate().setExternalExportsPrefix( "externalExportPrefix" );
		app.getTemplate().setDescription( "some description" );

		// Serialize things and generate the examples
		// (*) Applications
		writer = new StringWriter();
		mapper.writeValue( writer, app );
		String s = writer.toString();
		convertToTypes( s, Application.class, newDef );

		// (*) Application Templates
		writer = new StringWriter();
		mapper.writeValue( writer, app.getTemplate());
		s = writer.toString();
		convertToTypes( s, ApplicationTemplate.class, newDef );

		// Etc...

		return newDef;
	}


	/**
	 * @param newDef the new "definitions" object
	 * @throws IOException if something went wrong
	 */
	private void updateSwaggerJson( JsonObject newDef ) throws IOException {

		File f = new File( "target/docs/apidocs/ui/swagger.json" );
		if( ! f.exists())
			throw new RuntimeException( "The swagger.json file was not found." );

		JsonParser jsonParser = new JsonParser();
		String content = Utils.readFileContent( f );

		// Hack the file content directly here.
		// Do whatever raw operations you want.

		JsonElement jsonTree = jsonParser.parse( content );

		Set<String> currentTypes = new HashSet<> ();
		for( Map.Entry<String,JsonElement> entry : jsonTree.getAsJsonObject().get( "definitions" ).getAsJsonObject().entrySet()) {
			currentTypes.add( entry.getKey());
		}

		Set<String> newTypes = new HashSet<> ();
		for( Map.Entry<String,JsonElement> entry : newDef.entrySet()) {
			newTypes.add( entry.getKey());
		}

		currentTypes.removeAll( newTypes );
		for( String s : currentTypes ) {
			System.out.println( "Type not appearing in the updated swagger definitions: " + s );
		}

		Gson gson = new GsonBuilder().setPrettyPrinting().create();
		jsonTree.getAsJsonObject().add( "definitions", jsonParser.parse( gson.toJson( newDef )));
		String json = gson.toJson( jsonTree );
		Utils.writeStringInto( json, f );
	}


	/**
	 * Creates a JSon object from a serialization result.
	 * @param serialization the serialization result
	 * @param clazz the class for which this serialization was made
	 * @param newDef the new definition object to update
	 */
	public void convertToTypes( String serialization, Class<?> clazz, JsonObject newDef ) {
		convertToTypes( serialization, clazz.getSimpleName(), newDef );
		this.processedClasses.add( clazz );
	}


	/**
	 * Creates a JSon object from a serialization result.
	 * @param serialization the serialization result
	 * @param className a class or type name
	 * @param newDef the new definition object to update
	 */
	public void convertToTypes( String serialization, String className, JsonObject newDef ) {

		JsonParser jsonParser = new JsonParser();
		JsonElement jsonTree = jsonParser.parse( serialization );

		// Creating the swagger definition
		JsonObject innerObject = new JsonObject();

		// Start adding basic properties
		innerObject.addProperty( "title", className );
		innerObject.addProperty( "definition", "" );
		innerObject.addProperty( "type", jsonTree.isJsonObject() ? "object" : jsonTree.isJsonArray() ? "array" : "string" );

		// Prevent errors with classic Swagger UI
		innerObject.addProperty( "properties", "" );

		// Inner properties
		innerObject.add( "example", jsonTree.getAsJsonObject());

		// Update our global definition
		newDef.add( "json_" + className, innerObject );
	}
}

And that’s it.
In our implementation, we dropped the property keys of the swagger.json file. Instead, we use the example key. Swagger UI does display it well and does not show any error. Instead of showing some kind of schema, it shows an example of a JSon structure.

Testing the Coherence with our Mapper

How can we be sure this class generates examples for all the types we use in our custom object mapper? Well, let’s just write a unit test for that!

public class UpdateSwaggerJsonTest {

	@Test
	public void verifyProcessedClasses() throws Exception {

		UpdateSwaggerJson updater = new UpdateSwaggerJson();
		updater.prepareNewDefinitions();

		Set<Class<?>> classes = new HashSet<> ();

		// You need a registry somewhere that lists all the classes managed by your object mapper
		classes.addAll( JSonBindingUtils.SERIALIZERS.keySet());
		
		// Remove those you processed in the updater.
		classes.removeAll( updater.processedClasses );

		// They should all have been processed.
		Assert.assertEquals( Collections.emptySet(), classes );
	}
}

Just to show how you how we dealt with this class registry, here is a short snippet taken from our custom object mapper.

public final class JSonBindingUtils {

	public static final Map<Class<?>,? super JsonSerializer<?>> SERIALIZERS = new HashMap<> ();

	static {
		SERIALIZERS.put( Instance.class, new InstanceSerializer());
		SERIALIZERS.put( ApplicationTemplate.class, new ApplicationTemplateSerializer());
		SERIALIZERS.put( Application.class, new ApplicationSerializer());
	}


	/**
	 * Creates a mapper with specific binding for Roboconf types.
	 * @return a non-null, configured mapper
	 */
	@SuppressWarnings( { "unchecked", "rawtypes" } )
	public static ObjectMapper createObjectMapper() {

		ObjectMapper mapper = new ObjectMapper();
		mapper.configure( DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false );
		SimpleModule module = new SimpleModule( "RoboconfModule", new Version( 1, 0, 0, null, null, null ));

		for( Map.Entry<Class<?>,? super JsonSerializer<?>> entry : SERIALIZERS.entrySet())
			module.addSerializer((Class) entry.getKey(), (JsonSerializer) entry.getValue());

		mapper.registerModule( module );
		return mapper;
	}

Conclusion

This solution (or workaround) may not seem ideal.
However, custom object mappers are somehow a workaround themselves. They require you to code things. Therefore, it is not surprising we may have to code some little things to hook up with Swagger-based documentation.

IMO, the code shown here remains quite simple to maintain.
And it allows you to add unit tests to verify assertions on your swagger.json file.

I created a Gist for all the sources here. You can see them in action in the sources of Roboconf’s REST API.


About this entry