Skip to content

SaeedMasoumi/sweep-gson

Repository files navigation

Sweep Gson

CircleCI codecov Download

Sweep Gson adds (un)wrapping functionality to Gson without modifying any existing behavior.

Download

Gradle:

dependencies {
  implementation 'io.saeid.sweep:sweep-gson:1.0.0'
}

Usage

GsonBuilder().withSweep().create()

If you need more advance features:

GsonBuilder().withSweep {
      defaultWrapper = ...    // optional
      defaultUnwrapper = ...  // optional
      hooks = ...             // optional
}.create()

SweepWrapper

Use @SweepWrapper annotation to wrap the object with your desired value during serialization.

@SweepWrapper("request")
data class Request(val name : String)

The output after serializing the above class:

{
  "request" : {
    "name": "your_value"
  }
}

Nested Wrapping

@SweepWrapper also supports nested wrapping using dot as delimiter:

For instance, If you replace the value in the above example to @SweepWrapper("request.data"), It will generate:

{
  "request": {
    "data": {
      "name": "your_value"
    }
  }
}

Custom/Default Wrapping

If you want to use the class name as the wrapper value you can simply use @SweepWrapper(USE_CLASS_NAME_WRAPPER).

USE_CLASS_NAME_WRAPPER is a reserved word which will force @SweepWrapper to use the class name (decapitalized version) as the wrapper name.

For instance:

@SweepWrapper(USE_CLASS_NAME_WRAPPER)
data class Request(val name : String)
{
  "request" : {
    "name": "your_value"
  }
}

Also you can define the @SweepWrapper value at runtime by overriding defaultWrapper.

GsonBuilder().withSweep {
      defaultWrapper = object : DefaultWrapper {
         override fun <T> wrapWith(value: T): String? {
            return "request.$USE_CLASS_NAME_WRAPPER"
         }
      }
}.create()

Note: By default @SweepWrapper will switch to the defaultWrapper, If you don't pass any value.

SweepUnwrapper

Use @SweepUnwrapper annotation to unwrap the object with your desired value during deserialization. Unlike @SweepWrapper, @SweepUnwrapper only works on the root object.

{
  "response" : {
    "name": "your_value"
  }
}

For instance, The above JSON can be deserialized to the class below:

@SweepWrapper("response")
data class Response(val name : String)

Nested Unwrapping

@SweepUnwrapper also supports nested unwrapping using dot as delimiter:

For instance, If you replace the value in the above example to @SweepUnwrapper("response.body"), It can be extracted by the JSON below:

{
  "response": {
    "body": {
      "name": "your_value"
    }
  }
}

Custom/Default Unwrapping

Like @SweepWrapper, It supports USE_CLASS_NAME_UNWRAPPER.

Also you can define the @SweepUnwrapper value at runtime by overriding defaultUnwrapper.

GsonBuilder().withSweep {
      defaultUnwrapper = object : DefaultUnwrapper {
        override fun <T> unwrapWith(type: Class<T>): String? = null
      }
      
      override fun force() : Boolean = true

}.create()

@SweepUnwrapper also supports force-mode, which means It will unwrapp all objects event If they're not annotated with @SweepUnwrapper during deserialization.

If you want to disable force mode for a specific type, you can easily pass null.

Note: By default @SweepUnwrapper will switch to the defaultUnwrapper, If you don't pass any value.

startsWith/endsWith

@SweepUnwrapper also supports a simple starts/ends-With regex.

  • @SweepUnwrapper("*Response") It will unwrap everything ends with Response, e.g. singleResponse
  • @SweepUnwrapper("response*") It will unwrap everything starts with response, e.g. responseValue

Hooks

Sweep Gson allows to add an object to the root element before serialization by overriding addToRoot method from hooks:

GsonBuilder().withSweep {
      
      hooks = object : Hooks {
          override fun <T> addToRoot(value: T): Pair<String, Any>? {
             return Pair("properties", Properties(...)
          }
      }
}.create()

It will adds properties to the root classes annotated with SweepWrapper.

{
   "properties" : { 
       ... 
   }
   ...
}

Sample

Assume that you have an REST API with the request/response template below:

// request
{
    "properties": {
        "device": "user's device"
    },
    "request": {
        "request_type": {
	  }
    }
}

// response
{
    "response": {
         "response_typeReply": { 
         }
    }     
}

First create our DTOs:

@SweepWrapper
data class Login(val userName: String, val password: String)

@SweepUnwrapper
data class User(val name: String)

Then we create our Gson instance using withSweep:

    GsonBuilder().withSweep {
        // tell sweep gson to unwrap every object that match `response.*Reply`
        defaultUnwrapper = object : DefaultUnwrapper {
            override fun <T> unwrapWith(type: Class<T>): String? = "response.*Reply"
            override fun force(): Boolean = true
        }
        // tell sweep gson to wrap every annotated object with `request.[the class name of that object]`        
        defaultWrapper = object : DefaultWrapper {
            override fun <T> wrapWith(value: T): String = "request.$USE_CLASS_NAME_WRAPPER"
        }
        // add Properties to the root of our objects during serialization
        hooks = object : Hooks {
            override fun <T> addToRoot(value: T): Pair<String, Any>? {
                return Pair("properties", Properties("Android"))
            }
        }
    }.create()

And now the result:

gson.toJson(Login("admin", "admin")) 
// prints 
// {"properties":{"device":"Android"},"request":{"login":{"userName":"admin","password":"admin"}}}

gson.fromJson<User>("""{ "response": { "userReply": { "name":"admin" } } }""", User::class.java)
// prints
// User(name=admin)

Limitations

  • Unwrapper only unwraps from the root element.

For example, you can not deserialize the below JSON

{
  "parent": {
    "root" : {
      "name" : "sweep"
    }
  }
}

to

data class Root(val parent : Parent)

data class Parent(val child : Child)

@SweepUnwrapper("root")
data class Child(val name : String)
  • Unwrapper will ignore sibling elements while deserializing.

For example, version will be null after deserialization, but child will be deserialized.

{
  "parent": {
    "root" : {
      "name" : "sweep"
    }
  }
}
@SweepUnwrapper("root")
data class Root(val version : String, val child : Child)

data class Child(val name : String)
  • addToRoot only works If the root class is annotated with SweepWrapper.
  • Unlike SweepUnwrapper, there is no force mode available for SweepWrapper.