Skip to content

Azure Json Migration

Alan Zimmer edited this page Mar 29, 2024 · 5 revisions

Azure Json

com.azure:azure-json is a library that defines interfaces for reading and writing JSON using streaming APIs, or in other words, APIs where you explicitly define the behavior of the JSON being written. For example, a simple object with a single string property "hello":

jsonWriter.writeStartObject()
    .writeStringField("hello", "world")
    .writeEndObject();

com.azure:azure-json contains no external dependencies and provides a default implementation of the reader and writer interfaces.

Benefits

  1. azure-json contains no external dependencies, removing possible dependency conflicts with popular JSON serialization libraries such as Jackson and GSON.
  2. Stream serialization is easier to debug and reason about behavior as there isn't configurations hidden at construction time of the reader and writer nor does it need to perform reflective accesses to objects being deserialized.
  3. Reduces the amount of reflection and reflective access permissions, limiting possible access denied exceptions if the module system is configured incorrectly or a SecurityManager is being used to limit permissions during runtime.
  4. Possible reduction in JAR sizes and potential performance gains.
  5. Since reading and writing logic are interfaces the backing implementation could conform to the environment running the Azure SDK code. For example, you can use a GSON or Jackson implementation instead of the default.

Migration guide

As of January 24th 2024, azure-json is only supported by Swagger code generation. TypeSpec code generation doesn't have the one-off per-SDK code generation configurations that Swagger does, it will be enabled in a future release.

Migration to azure-json is dependent on the library being migrated. Code generated libraries will require Autorest configuration changes to enable generating code with azure-json. Handwritten libraries, or portions of code generated libraries that were handwritten, will require manual intervention.

As part of this migration, you'll be implementing (either through code generation or manually) JsonSerializable<T> on all your models and removing the ability for Jackson Databind to reflectively access and serialize your models (functionality replaced by JsonSerializable<T>).

Update project configurations

Start by adding the following dependency to your project's pom.xml.

<dependency>
  <groupId>com.azure</groupId>
  <artifactId>azure-json</artifactId>
  <version>1.1.0</version> <!-- {x-version-update;com.azure:azure-json;dependency} -->
</dependency>

Then add the following requires to your project's module-info.java.

requires com.azure.json;

Also, in the project's module-info.java, ensure that all packages that currently opens to com.fasterxml.jackson,databind opens to com.azure.core and they remove the opens to com.fasterxml.jackson.databind. This will ensure two things:

  1. azure-core will be able to deserialize your models using JsonSerializable.fromJson(JsonReader) that is required for all serializable models using azure-json.
  2. Result in a reflection failure if Jackson Databind continues to be used to serialize or deserialize your models, which is being removed as part of this migration.

For example, if you had:

opens com.azure.sdk.models to com.fasterxml.jackson.databind;
opens com.azure.sdk.implementation to com.fasterxml.jackson.databind, com.azure.core;

it becomes:

opens com.azure.sdk.models to com.azure.core;
opens com.azure.sdk.implementation to com.azure.core;

If your POM has the property javaModulesSurefireArgLine configured, remove any --add-exports, --add-opens, and --add-reads using com.fasterxml.jackson.databind.

Update code generation

Updating code generation may not apply to every library.

In your Autorest configuration, enabling azure-json is just adding the configuration stream-style-serialization: true. In a future release of Autorest this will become the default configuration.

Additionally, at this time if you want, add the use configuration to your Autorest configurations. This configures which version of Autorest Java to use in code generation without having to pass it each time in the command line using --use=<version>. View here for the latest versions of Autorest Java: https://github.com/Azure/autorest.java/releases. An example of the configuration is use: '@autorest/java@4.1.26'.

Additionally, you can view the template project's Swagger configuration for a good set of default configurations to use:

https://github.com/Azure/azure-sdk-for-java/blob/main/sdk/template/azure-sdk-template/swagger/README.md#configuration

Update handwritten code

Updating handwritten code requires removing usage of Jackson as much as possible, ideally completely. Unfortunately, this step isn't as quick, but use the following as ways to help scope work.

  1. In your project search for all references of com.fasterxml.jackson. This will find spots where Jackson is being used.
  2. Follow the deserialization logic used by generated code.

If you have questions reach out to the Azure SDK for Java team, we'll be more than happy to help.

Example

Code using Jackson annotations.

public final class FileSystemEncryptionScopeOptions {

    /*
     * Optional.  Version 2021-06-08 and later. Specifies the default
     * encryption scope to set on the container and use for all future writes.
     */
    @JsonProperty(value = "DefaultEncryptionScope")
    private String defaultEncryptionScope;

    /*
     * Optional.  Version 2021-06-08 and newer. If true, prevents any request
     * from specifying a different encryption scope than the scope set on the
     * container.
     */
    @JsonProperty(value = "EncryptionScopeOverridePrevented")
    private Boolean encryptionScopeOverridePrevented;
}

Code migrated to azure-json.

public final class FileSystemEncryptionScopeOptions implements JsonSerializable<FileSystemEncryptionScopeOptions> {

    /*
     * Optional.  Version 2021-06-08 and later. Specifies the default
     * encryption scope to set on the container and use for all future writes.
     */
    private String defaultEncryptionScope;

    /*
     * Optional.  Version 2021-06-08 and newer. If true, prevents any request
     * from specifying a different encryption scope than the scope set on the
     * container.
     */
    private Boolean encryptionScopeOverridePrevented;

    @Override
    public JsonWriter toJson(JsonWriter jsonWriter) throws IOException {
        jsonWriter.writeStartObject();
        jsonWriter.writeStringField("DefaultEncryptionScope", defaultEncryptionScope);
        jsonWriter.writeBooleanField("EncryptionScopeOverridePrevented", encryptionScopeOverridePrevented);
        jsonWriter.writeEndObject();
        return jsonWriter;
    }

    /**
     * Reads a JSON stream into a {@link FileSystemEncryptionScopeOptions}.
     * 
     * @param jsonReader The {@link JsonReader} being read.
     * @return The {@link FileSystemEncryptionScopeOptions} that the JSON stream represented, or null if it pointed to JSON null.
     * @throws IOException If an I/O error occurs.
     */
    public static FileSystemEncryptionScopeOptions fromJson(JsonReader jsonReader) throws IOException {
        return jsonReader.readObject(reader -> {
            FileSystemEncryptionScopeOptions fileSystemEncryptionScopeOptions = new FileSystemEncryptionScopeOptions();
            
            while (reader.nextToken() != JsonToken.END_OBJECT) {
                String fieldName = reader.getFieldName(); // Get the name of the field.
                reader.nextToken(); // Progress to the value.

                if ("DefaultEncryptionScope".equals(fieldName)) {
                    fileSystemEncryptionScopeOptions.defaultEncryptionScope = reader.getString();
                } else if ("EncryptionScopeOverridePrevented".equals(fieldName)) {
                    fileSystemEncryptionScopeOptions.encryptionScopeOverridePrevented = reader.getNullable(JsonReader::getBoolean);
                } else {
                    // Skip unknown values.
                    // If the type supported additional properties, this is where they would be handled.
                    reader.skipChildren();
                }
            }

            return fileSystemEncryptionScopeOptions;
        });
    };
}

Note: toJson doesn't need to call JsonWriter.flush() as that is handled by the caller.

Update build

There are two things that may require updating after completing code migration.

  1. Stream serialization is more verbose and is lines of code instead of annotations like Jackson Databind uses. Your code coverage will likely drop after migration, meaning either more testing needs to be added to missed lines or required code coverage needs to be reduced.
  2. RevApi considers removal of Jackson annotations to be a breaking change. You'll need to update the RevApi suppressions file to ignore these API breaks. Here is an example of current suppressions, they should be easily copied and updated to fit your needs:
{
  "regex": true,
  "code": "java\\.annotation\\.removed",
  "old": ".*? com\\.azure\\.ai\\.metricsadvisor\\.(administration\\.)?models.*",
  "justification": "Removing Jackson annotations from Metrics Advisor in transition to stream-style."
},
{
  "regex": true,
  "code": "java\\.annotation\\.removed",
  "old": ".*? com\\.azure\\.messaging\\.eventgrid\\.systemevents.*",
  "justification": "Removing Jackson annotations from EventGrid in transition to stream-style."
}

https://github.com/Azure/azure-sdk-for-java/blob/main/eng/code-quality-reports/src/main/resources/revapi/revapi.json#L328

Finalization

Finally, just run your test suite, both in playback and live testing, to make sure there are no regressions or bugs introduced as part of migration.

Clone this wiki locally