Repository: serj-lotutovici/moshi-lazy-adapters Branch: master Commit: 5f2d890c5c5d Files: 63 Total size: 177.5 KB Directory structure: gitextract_k8ld05jz/ ├── .buildscript/ │ └── deploy_snapshot.sh ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.gradle ├── checkstyle.gradle ├── config/ │ └── checkstyle/ │ └── checkstyle.xml ├── dependencies.gradle ├── gradle/ │ ├── gradle-mvn-push.gradle │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jacoco.gradle ├── settings.gradle └── src/ ├── integrationTest/ │ └── java/ │ └── com/ │ └── serjltt/ │ └── moshi/ │ └── adapters/ │ ├── Data.java │ ├── DataFactories.java │ ├── DeserializeOnlyAutoValueTest.java │ ├── LazyAdaptersAutoValueTest.java │ ├── LazyAdaptersRetrofitTest.java │ ├── LazyAdaptersRxJavaTest.java │ ├── Nullable.java │ ├── SerializeOnlyAutoValueTest.java │ └── TransientAutoValueTest.java ├── main/ │ └── java/ │ └── com/ │ └── serjltt/ │ └── moshi/ │ └── adapters/ │ ├── DefaultOnDataMismatchAdapter.java │ ├── DeserializeOnly.java │ ├── ElementAt.java │ ├── ElementAtJsonAdapter.java │ ├── FallbackEnum.java │ ├── FallbackEnumJsonAdapter.java │ ├── FallbackOnNull.java │ ├── FallbackOnNullJsonAdapter.java │ ├── FilterNulls.java │ ├── FilterNullsJsonAdapter.java │ ├── FirstElement.java │ ├── LastElement.java │ ├── LastElementJsonAdapter.java │ ├── Pair.java │ ├── SerializeNulls.java │ ├── SerializeOnly.java │ ├── SerializeOnlyNonEmpty.java │ ├── SerializeOnlyNonEmptyJsonAdapter.java │ ├── Transient.java │ ├── TransientJsonAdapter.java │ ├── Util.java │ ├── Wrapped.java │ └── WrappedJsonAdapter.java └── unitTest/ └── java/ └── com/ └── serjltt/ └── moshi/ └── adapters/ ├── Custom.java ├── DefaultOnDataMismatchAdapterTest.java ├── DeserializeOnlyJsonAdapterTest.java ├── ElementAtJsonAdapterTest.java ├── FallbackEnumJsonAdapterTest.java ├── FallbackOnNullJsonAdapterTest.java ├── FilterNullsJsonAdapterTest.java ├── FirstElementJsonAdapterTest.java ├── LastElementJsonAdapterTest.java ├── SerializeNullsJsonAdapterTest.java ├── SerializeOnlyJsonAdapterTest.java ├── SerializeOnlyNonEmptyJsonAdapterTest.java └── WrappedJsonAdapterTest.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .buildscript/deploy_snapshot.sh ================================================ #!/bin/bash # # Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo. # # Adapted from https://coderwall.com/p/9b_lfq and # http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/ SLUG="serj-lotutovici/moshi-lazy-adapters" BRANCH="master" set -e if [ "$TRAVIS_REPO_SLUG" != "$SLUG" ]; then echo "Skipping snapshot deployment: wrong repository. Expected '$SLUG' but was '$TRAVIS_REPO_SLUG'." elif [ "$TRAVIS_PULL_REQUEST" != "false" ]; then echo "Skipping snapshot deployment: was pull request." elif [ "$TRAVIS_BRANCH" != "$BRANCH" ]; then echo "Skipping snapshot deployment: wrong branch. Expected '$BRANCH' but was '$TRAVIS_BRANCH'." else echo "Deploying snapshot..." ./gradlew clean uploadArchives -PnexusUsername="$TRAVIS_USER_NAME" -PnexusPassword="$TRAVIS_PASSWORD" echo "Snapshot deployed!" fi ================================================ FILE: .gitignore ================================================ # Gradle junk .gradle/ build/ local.properties # Idea junk .idea/ *.iml out/ # MacOS junk .DS_Store ================================================ FILE: .travis.yml ================================================ language: java jdk: - oraclejdk8 script: - ./gradlew clean check --stacktrace after_success: - bash <(curl -s https://codecov.io/bash) - .buildscript/deploy_snapshot.sh env: global: - secure: "PD2ZaZxyrpLDq8dsC+JdfkGg7kEPgmhBzqO+zr+ONBuVe+YDLqsTAUF6J6zXoMF6mgBvkalW/v1nRbDvKJBXhGJdrIqPoQ2yhc7nPH1shzUK6WFptsRsPk2+B45M+FmAjlsSHqCjXIcPmRKykCjgjFyR6DVsik2mPA/C8dINwM2LUsD6eKsed79VHjhrpmqBfIKsvSY1saGfqlGmNzYOFHJ8wHfgjBVUlJmzJzWM49gudW8J27zphP59GjAR1/ZHeYAhE+KQEMUfMKtpFUjZiWvE5kNd+4xgUfXh9mRpwA/RuCJhipgzMTBPxHF4WHHC4pIQDJ5/KeF++L6xzweuBheGvOewWvjSWnPpBsEk3YK6X0SI52OcAApsk1KX+8h58ABU/85DObFpxLsAYTNnePWc6f4a1boV/K2f/RTZBp9U15h9z7474BXIZ7t/IFCZgPZp+v5a15CJrjBq/8g1Yi7f0KGj6ax5vctRaQK5pvIasT1Z0bZLWoRO8WWOkatymkUhBT/aPiVm3PbZeqLgp+tljiSzCF6oF7rQDjbCHsNOAr9Y1+SS/bIu0sLXCgQ4Ji4HGDYpub3v6vRWMW5sv23LR9YsgVXXaehZR1k4hScqbscu6/FfYy5N/2h8IeABNZW3mYnR6zmk1VM4uzHcMkD9iJAc07tPsmrxd4aOaNQ=" - secure: "fiI2ZFtZ1LRVcZizbbiHCiKrtKkcyzAPZse8quvuB3VumnfqhPTT1hyyRkV3vXP6R6oigdZG67pG2yGvMovHfyMfQfOfTyfyQw/i7xJTTgOkH2NmeTA+SPn/VnZjZPSwyvkDXgxOOjyoa3G7eSTpblzh13247tZppA4ERKyUUmHPDBVL0kktkabQjRltu6vuUT1vLh0j5Lg/A5HRWF22Bkt0NqrmkYbJaDaW40w/f62OonXS8g4LIKolmHFIfhyoMG4Pj1gUAPUl+0mJ6d/CkCYxnRmWSzTvodsYCp+y06e9MXLL95A+2wg/5oUrwOPigwMlWDp0rWgceU9X0wWK6+7I7nWCJb/+Hr+okL/UE62BxnDPWecyvXRsGbXHAx/VXv2dP3f4qi9GMn2NnpgswQZUdFLfBzZTghT9UftaLeUej6rWrnnlEJLr78ZyYPvXVtwXm0oAWH0iA39Nd6OCmV+YIqgnptUzWugHKCbR4/bgs02HvuIfw+bRGEYUshjD3gSZYNYKcuV8TYmFOnQqRXaYm4yosxwX/t6Zp1mH+s6N98OEEYHIwyaQ8hxBKrpAWmVYr0Nzu7rfITNwLkTB2LBHlv3K2zYP8vz8mz3gm5Y6VFEUqbnHLIfLDWw8jy0Q4oZSyzytgrKeQTeI0erku2uOJFzRce6bHmEjRfOLwDM=" branches: except: - gh-pages notifications: email: false cache: directories: - $HOME/.gradle ================================================ FILE: CHANGELOG.md ================================================ Change Log === 2.2 *(02-05-2018)* --- * Usage selectString in FallbackEnumJsonAdapter. (#64) * Fix FallbackOnNull naming bug with implicit user locale. (#66) 2.1 *(11-10-2017)* --- * Fix: Forces WrappedJsonAdapter to re-throw all exceptions. (#60) 2.0 *(06-06-2017)* --- * New: Move all adapter factories to their respective annotations. (#47) * New: Added `DefaultOnDataMismatchAdapter`. (#54) * New: Added `@FilterNulls` annotation and its respective adapter. (#52) * New: Added `@LastElement` annotation plus adapter (#50) * New: Added `@Transient` annotation and it's respective adapter. (#49) * New: Makes `JsonAdapter` for `@Wrapped` more strict. (#45) * Enhancement: Rely on moshi's resialize nulls functionality. (#46) * Enhancement: Use moshi's Types.nextAnnotations() where possible. (#44) * New: Upgrade to Moshi 1.5.0. ``` compile 'com.squareup.moshi:moshi:1.5.0' ``` 1.3 *(04-01-2017)* --- * New: Added `SerializeOnlyNonEmpty` for all collections and arrays. * New: Allow `WrappedJsonAdapter` to fail on un-found value. * New: Upgrade to Moshi 1.3.0. ``` compile 'com.squareup.moshi:moshi:1.3.0' ``` 1.2 *(08-11-2016)* --- * New: `@FallbackEnum` annotation and it's respective adapter. * New: `@ElementAt` (similar to `@FirstElement` but more powerful) and it's respective adapter. * Fix: `@SerializeNulls` adapter now maintains previous writer setting. 1.1 *(17-10-2016)* --- * New: Rename `@UnwrapJson` to `@Wrapped` annotation and it's respective adapter to `WrappedJsonAdapter`. * New: Allow manual creation of `@Wrapped` annotation via `Wrapped.Factory`. * New: `@SerializeOnly` & `@DeserializeOnly` annotations and their respective adapters. * New: Upgrade to Moshi 1.3.0. ``` compile 'com.squareup.moshi:moshi:1.3.0' ``` * Fix: Restrict `@Target` for each declared annotation. 1.0 *(14-09-2016)* --- * Initial release. * Introduce few handy adapters: * **SerializeNullsJsonAdapter** - Instructs moshi to serialize a value even if it's `null`; * **FirstElementJsonAdapter** - Instructs moshi to retrieve only the first element of a list; * **UnwrapJsonAdapter** - Unwraps a json object under the specified path; * **FallbackOnNullJsonAdapter** - Instructs moshi to fallback to a default value in case the json field is `null`. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2016 Serj Lotutovici Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ Moshi Lazy Adapters === [![Build Status][travis.svg]][travis] [![codecov][codecov.svg]][codecov] [![Latest Build][latestbuild.svg]][latestbuild] [![Android Arsenal][arsenal.svd]][arsenal] A collection of simple JsonAdapters for [Moshi][moshi]. This library acts as an extension to Moshi by providing general purpose, yet useful `JsonAdapter`s that are not present in the main library. Most provided adapters are linked via specialized `JsonQualifier` annotations that alter serialization/deserialization strategies. ##### How To Use It This library is **not** forcing any of it's own adapters by default. To leverage from any of the provided annotations/adapters add their respective factory to your `Moshi.Builder`: ```java // Creates a Moshi instance with an adapter that will handle the @Wrapped annotations. Moshi moshi = new Moshi.Builder() .add(Wrapped.ADAPTER_FACTORY) .build(); ``` ##### Overall contract Every declared annotation/adapter in this library (unless specified in the class's documentation) supports adapter composition. Which means that no `JsonAdapter.Factory` will short circuit adapter construction on it's own annotation, and instead will delegate to the next adapter returned by `Moshi`. Meaning that given the following type declaration: ```java interface WebService { @GET("/data") @Wrapped(path={"result", "data"}) @FirstElement MyData getData(); } ``` The resulting `JsonAdapter` will unwrap a list of `MyData` from the json response and return only the first element of that list. One **important** concept to keep in mind, that the order of declared annotations in the example above **does not have any influence** on the way how the final adapter will be constructed. Instead **the order** of the `JsonAdapter.Factory`'s added to the `Moshi` instance is what plays a major role in overall behavior. Meaning that in order for the example above to satisfy the expected result, one must add the factories in the following order: ``` Moshi moshi = new Moshi.Builder() .add(Wrapped.ADAPTER_FACTORY) .add(FirstElement.ADAPTER_FACTORY) .build() ``` Some Lazy Adapters --- ### `@Wrapped` Some apis enjoy wrapping the response object inside other json objects. This creates a lot of inconvenience when it comes to consuming the request. The following example contains a list of a users favorite pokemon which is wrapped behind two keys: ```json { "favorite_pokemon": { "pokemons": [ "Snorlax", "Pikachu", "Bulbasaur", "Charmander", "Squirtle" ] } } ``` In the end the consumer is just interested in the names of the pokemon, but the json object forces to create a wrapping object, which will contain the list: ```java class FavoritePokemonResponse { FavoritePokemon favorite_pokemon; } class FavoritePokemon { List pokemons; } ``` A custom adapter would be another option, and `WrappedJsonAdapter` is just the one. By annotating the response type with `@Wrapped` and providing the path to the desired list, the need for an additional object is dropped: ```java // This assumes that Retrofit is used to obtain the response. interface PokemonService { @GET("/pokemon/favorite") @Wrapped({"favorite_pokemon", "pokemons"}) Call> getFavorite(); } ``` No need for a new class, which results in less code and less methods generated by the consumer code. You can also annotate any field in your response entity and the same rules will apply. ### `@FallbackOnNull` Primitives are simple and safe. Primitives have also a smaller memory footprint. Some apis may return `null` for values that are normally processed as primitives. A safe alternative would be to use their boxed counterparts, but that would result in redundant boxing and unboxing. By annotating any primitive field with `@FallbackOnNull` the consumer can specify a default value for the field, in case it's json representation is null. ```json [ { "name": "Pikachu", "number_of_wins": 1 }, { "name": "Magikarp", "number_of_wins": null } ] ``` The json above contains a list of pokemon of a user with their names and the number of wins the respective pokemon has obtained during it's trainings of fights. Notice that the 'Magikarp' pokemon has `null` wins. Normally the representing POJO would declare the filed as `Integer`, but #perfmatters. With `@FallbackOnNull` the `Pokemon` object can be declared as: ```java class Pokemon { String name; @FallbackOnNull int number_of_wins; } ``` If the incoming value would be `null` the `number_of_wins` field would default to `Integer.MIN_VALUE`, this can be altered by providing an alternative fallback: ```java @FallbackOnNull(fallbackInt = -1) int number_of_wins; ``` See [FallbackOnNull's documentation](../master/src/main/java/com/serjltt/moshi/adapters/FallbackOnNull.java) for a full reference. List of provided Adapters --- * **DefaultOnDataMismatchAdapter** - Allows the consumer to provided a default fallback value for any type. * **SerializeNulls (annotation)** - Serializes a value even if it's `null`; * **FirstElement (annotation)** - Deserializes only the first element of a list. * **LastElement (annotation)** - Deserializes only the last element of a list. * **ElementAt (annotation)** - Deserializes an element from a specified position of a list. * **FallbackOnNull (annotation)** - Fallbacks to a default value in case the json field is `null`. * **FallbackEnum (annotation)** - Fallbacks to a default enum value if the parsed value can not be matched to an existing one. * **Wrapped (annotation)** - Unwraps a json object under the specified path when parsing, and wraps it when serializing to json. * **SerializeOnly (annotation)** - Only serializes the annotated field, and ignores it during deserialization. * **DeserializeOnly (annotation)** - Only deserializes the annotated field, and ignores it during serialization. * **Transient (annotation)** - (Targets methods only) indicates that a field should be ignored for serialization/deserialization. * **SerializeOnlyNonEmpty (annotation)** - Will serialize a collection or array only if it contains at-least one value. Download --- Download [the latest JAR][dl] or depend via Maven: ```xml com.serjltt.moshi moshi-lazy-adapters x.y ``` or Gradle: ```groovy compile 'com.serjltt.moshi:moshi-lazy-adapters:x.y' ``` Snapshots of the development version are available in [Sonatype's `snapshots` repository][sonatype]. License === Copyright 2017 Serj Lotutovici Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. [moshi]: https://github.com/square/moshi [travis]: https://travis-ci.org/serj-lotutovici/moshi-lazy-adapters [travis.svg]: https://travis-ci.org/serj-lotutovici/moshi-lazy-adapters.svg?branch=master [codecov]: https://codecov.io/gh/serj-lotutovici/moshi-lazy-adapters [codecov.svg]: https://codecov.io/gh/serj-lotutovici/moshi-lazy-adapters/branch/master/graph/badge.svg [latestbuild]: http://search.maven.org/#search%7Cga%7C1%7Ccom.serjltt.moshi [latestbuild.svg]: https://img.shields.io/maven-central/v/com.serjltt.moshi/moshi-lazy-adapters.svg [arsenal.svd]: https://img.shields.io/badge/Android%20Arsenal-Moshi%20Lazy%20Adapters-orange.svg?style=flat [arsenal]: http://android-arsenal.com/details/1/4481 [sonatype]: https://oss.sonatype.org/content/repositories/snapshots/com/serjltt/moshi/ [dl]: https://search.maven.org/remote_content?g=com.serjltt.moshi&a=moshi-lazy-adapters&v=LATEST ================================================ FILE: build.gradle ================================================ buildscript { repositories { mavenCentral() maven { url 'https://jitpack.io' } } // Needed to use auto-value-moshi in tests dependencies { classpath 'com.github.tbroyer:gradle-apt-plugin:v0.12' } } repositories { mavenCentral() } apply plugin: 'net.ltgt.apt-idea' apply plugin: 'java' apply from: rootProject.file('dependencies.gradle') apply from: rootProject.file('checkstyle.gradle') apply from: rootProject.file('jacoco.gradle') apply from: rootProject.file('gradle/gradle-mvn-push.gradle') sourceCompatibility = javaVersion targetCompatibility = javaVersion sourceSets { // Setup source sets to split unit and integration tests test { java.srcDirs = ['src/unitTest/java', 'src/integrationTest/java'] resources.srcDirs = ['src/unitTest/resources', 'src/integrationTest/resources'] } } dependencies { compile moshi testCompile junit testCompile assertJ testCompile privateConstructorChecker testCompile retrofit testCompile retrofitMoshiConverter testCompile mockWebServer testCompile rxJava2 // This is needed to test integration with auto-value-moshi testCompileOnly autoValueMoshiAnnotations testCompileOnly autoValueAnnotations testApt autoValueMoshi } ================================================ FILE: checkstyle.gradle ================================================ apply plugin: 'checkstyle' checkstyle { toolVersion '6.0' } task checkstyle(type: Checkstyle) { configFile rootProject.file('config/checkstyle/checkstyle.xml') source = ['src'] ignoreFailures false showViolations true include '**/*.java' classpath = files() configProperties = [ 'proj.module.dir' : projectDir.absolutePath, 'checkstyle.cache.file': './build/cache/checkstyle-cache' ] } afterEvaluate { if (project.tasks.findByName('check')) { check.dependsOn('checkstyle') } } ================================================ FILE: config/checkstyle/checkstyle.xml ================================================ ================================================ FILE: dependencies.gradle ================================================ ext { javaVersion = JavaVersion.VERSION_1_7 ci = 'true'.equals(System.getenv('CI')) /* Dependencies */ moshi = 'com.squareup.moshi:moshi:1.5.0' /* Testing */ junit = 'junit:junit:4.12' assertJ = 'org.assertj:assertj-core:2.5.0' retrofit = 'com.squareup.retrofit2:retrofit:2.1.0' retrofitMoshiConverter = 'com.squareup.retrofit2:converter-moshi:2.1.0' mockWebServer = 'com.squareup.okhttp3:mockwebserver:3.4.1' privateConstructorChecker = 'com.pushtorefresh.java-private-constructor-checker:checker:1.2.0' rxJava2 = 'io.reactivex.rxjava2:rxjava:2.0.3' autoValueMoshi = 'com.ryanharter.auto.value:auto-value-moshi:0.4.2' autoValueMoshiAnnotations = 'com.ryanharter.auto.value:auto-value-moshi-annotations:0.4.2' autoValueAnnotations = 'com.jakewharton.auto.value:auto-value-annotations:1.3' } ================================================ FILE: gradle/gradle-mvn-push.gradle ================================================ /* * Copyright (C) 2013 Chris Banes * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import static org.gradle.internal.jvm.Jvm.current apply plugin: 'maven' apply plugin: 'signing' version = VERSION_NAME group = GROUP def isSnapshot = VERSION_NAME.endsWith('-SNAPSHOT') def ossrhUsername = hasProperty('nexusUsername') ? nexusUsername : "" def ossrhPassword = hasProperty('nexusPassword') ? nexusPassword : "" afterEvaluate { project -> uploadArchives { repositories { mavenDeployer { beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } pom.groupId = GROUP pom.artifactId = POM_ARTIFACT_ID pom.version = VERSION_NAME repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/') { authentication(userName: ossrhUsername, password: ossrhPassword) } snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { authentication(userName: ossrhUsername, password: ossrhPassword) } pom.project { name POM_NAME packaging POM_PACKAGING description POM_DESCRIPTION url POM_URL scm { url POM_SCM_URL connection POM_SCM_CONNECTION developerConnection POM_SCM_DEV_CONNECTION } licenses { license { name POM_LICENCE_NAME url POM_LICENCE_URL distribution POM_LICENCE_DIST } } developers { developer { id POM_DEVELOPER_ID name POM_DEVELOPER_NAME } } } } } } signing { required { !isSnapshot && gradle.taskGraph.hasTask("uploadArchives") } sign configurations.archives } if (project.getPlugins().hasPlugin('com.android.application') || project.getPlugins().hasPlugin('com.android.library')) { uploadArchives.dependsOn(build) task install(type: Upload, dependsOn: assemble) { group 'upload' repositories.mavenInstaller { configuration = configurations.archives pom.groupId = GROUP pom.artifactId = POM_ARTIFACT_ID pom.version = VERSION_NAME pom.project { name POM_NAME packaging POM_PACKAGING description POM_DESCRIPTION url POM_URL scm { url POM_SCM_URL connection POM_SCM_CONNECTION developerConnection POM_SCM_DEV_CONNECTION } licenses { license { name POM_LICENCE_NAME url POM_LICENCE_URL distribution POM_LICENCE_DIST } } developers { developer { id POM_DEVELOPER_ID name POM_DEVELOPER_NAME } } } } } task androidJavadocs(type: Javadoc) { source = android.sourceSets.main.java.source classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) } task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { classifier = 'javadoc' from androidJavadocs.destinationDir } task androidSourcesJar(type: Jar) { classifier = 'sources' from android.sourceSets.main.java.source } } else { install { repositories.mavenInstaller { pom.groupId = GROUP pom.artifactId = POM_ARTIFACT_ID pom.version = VERSION_NAME pom.project { name POM_NAME packaging POM_PACKAGING description POM_DESCRIPTION url POM_URL scm { url POM_SCM_URL connection POM_SCM_CONNECTION developerConnection POM_SCM_DEV_CONNECTION } licenses { license { name POM_LICENCE_NAME url POM_LICENCE_URL distribution POM_LICENCE_DIST } } developers { developer { id POM_DEVELOPER_ID name POM_DEVELOPER_NAME } } } } } task sourcesJar(type: Jar, dependsOn: classes) { classifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn: javadoc) { classifier = 'javadoc' //noinspection GroovyAccessibility from javadoc.destinationDir } } // JDK 1.8 is more strict then 1.7. Have JDK 1.8 behave like 1.7 for javadoc generation if (current().getJavaVersion() == JavaVersion.VERSION_1_8) { allprojects { tasks.withType(Javadoc) { options.addStringOption('Xdoclint:none', '-quiet') } } } artifacts { if (project.getPlugins().hasPlugin('com.android.application') || project.getPlugins().hasPlugin('com.android.library')) { archives androidSourcesJar archives androidJavadocsJar } else { archives sourcesJar archives javadocJar } } /** Task that allows to install archives to local maven. */ task installArchives(type: Upload) { group 'upload' description "Installs the artifacts to the local Maven repository." configuration = configurations['archives'] repositories { mavenDeployer { pom.groupId = GROUP pom.artifactId = POM_ARTIFACT_ID pom.version = VERSION_NAME repository url: "file://${System.properties['user.home']}/.m2/repository" pom.project { name POM_NAME packaging POM_PACKAGING description POM_DESCRIPTION url POM_URL } } } } } ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ #Mon Oct 09 10:31:26 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-all.zip ================================================ FILE: gradle.properties ================================================ GROUP=com.serjltt.moshi VERSION_NAME=2.3-SNAPSHOT POM_DESCRIPTION=A collection of simple JsonAdapters for Moshi. POM_ARTIFACT_ID=moshi-lazy-adapters POM_NAME=Moshi Lazy Adapters POM_PACKAGING=jar POM_URL=https://github.com/serj-lotutovici/moshi-lazy-adapters/ POM_SCM_URL=https://github.com/serj-lotutovici/moshi-lazy-adapters POM_SCM_CONNECTION=scm:git:git@github.com:serj-lotutovici/moshi-lazy-adapters.git POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com:serj-lotutovici/moshi-lazy-adapters.git POM_LICENCE_NAME=The Apache Software License, Version 2.0 POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt POM_LICENCE_DIST=repo POM_DEVELOPER_ID=serj-lotutovici POM_DEVELOPER_NAME=Serj Lotutovici ================================================ FILE: gradlew ================================================ #!/usr/bin/env sh ############################################################################## ## ## Gradle start up script for UN*X ## ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link PRG="$0" # Need this for relative symlinks. while [ -h "$PRG" ] ; do ls=`ls -ld "$PRG"` link=`expr "$ls" : '.*-> \(.*\)$'` if expr "$link" : '/.*' > /dev/null; then PRG="$link" else PRG=`dirname "$PRG"`"/$link" fi done SAVED="`pwd`" cd "`dirname \"$PRG\"`/" >/dev/null APP_HOME="`pwd -P`" cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" warn ( ) { echo "$*" } die ( ) { echo echo "$*" echo exit 1 } # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false case "`uname`" in CYGWIN* ) cygwin=true ;; Darwin* ) darwin=true ;; MINGW* ) msys=true ;; NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables JAVACMD="$JAVA_HOME/jre/sh/java" else JAVACMD="$JAVA_HOME/bin/java" fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else JAVACMD="java" which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi # Increase the maximum file descriptors if we can. if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then MAX_FD="$MAX_FD_LIMIT" fi ulimit -n $MAX_FD if [ $? -ne 0 ] ; then warn "Could not set maximum file descriptor limit: $MAX_FD" fi else warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" fi fi # For Darwin, add options to specify how the application appears in the dock if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi # For Cygwin, switch paths to Windows format before running java if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` SEP="" for dir in $ROOTDIRSRAW ; do ROOTDIRS="$ROOTDIRS$SEP$dir" SEP="|" done OURCYGPATTERN="(^($ROOTDIRS))" # Add a user-defined pattern to the cygpath arguments if [ "$GRADLE_CYGPATTERN" != "" ] ; then OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" fi # Now convert the arguments - kludge to limit ourselves to /bin/sh i=0 for arg in "$@" ; do CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` else eval `echo args$i`="\"$arg\"" fi i=$((i+1)) done case $i in (0) set -- ;; (1) set -- "$args0" ;; (2) set -- "$args0" "$args1" ;; (3) set -- "$args0" "$args1" "$args2" ;; (4) set -- "$args0" "$args1" "$args2" "$args3" ;; (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi # Escape application args save ( ) { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } APP_ARGS=$(save "$@") # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then cd "$(dirname "$0")" fi exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @rem @rem ########################################################################## @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS= @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if "%ERRORLEVEL%" == "0" goto init echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto init echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo. echo Please set the JAVA_HOME variable in your environment to match the echo location of your Java installation. goto fail :init @rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args :win9xME_args @rem Slurp the command line arguments. set CMD_LINE_ARGS= set _SKIP=2 :win9xME_args_slurp if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% :end @rem End local scope for the variables with windows NT shell if "%ERRORLEVEL%"=="0" goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 exit /b 1 :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: jacoco.gradle ================================================ apply plugin: 'jacoco' jacoco { toolVersion = '0.7.7.201606060606' // See http://www.eclemma.org/jacoco/. } jacocoTestReport { reports { xml.enabled = true html.enabled = true } } check.dependsOn jacocoTestReport ================================================ FILE: settings.gradle ================================================ rootProject.name = 'moshi-lazy-adapters' ================================================ FILE: src/integrationTest/java/com/serjltt/moshi/adapters/Data.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.google.auto.value.AutoValue; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; /** Data class for testing auto-value-moshi integration. */ @AutoValue abstract class Data { public static JsonAdapter jsonAdapter(Moshi moshi) { return new AutoValue_Data.MoshiJsonAdapter(moshi); } abstract String name(); /** * The name of the method will be taken as the first key, then the path provided with the * annotation. */ @Wrapped(path = {"1"}) abstract Meta meta(); static class Meta { String value1; int value2; } } ================================================ FILE: src/integrationTest/java/com/serjltt/moshi/adapters/DataFactories.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.ryanharter.auto.value.moshi.MoshiAdapterFactory; import com.squareup.moshi.JsonAdapter; /** Provided the factory for generated adapters. */ @MoshiAdapterFactory abstract class DataFactories implements JsonAdapter.Factory { public static JsonAdapter.Factory create() { return new AutoValueMoshi_DataFactories(); } } ================================================ FILE: src/integrationTest/java/com/serjltt/moshi/adapters/DeserializeOnlyAutoValueTest.java ================================================ package com.serjltt.moshi.adapters; import com.google.auto.value.AutoValue; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import java.io.IOException; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public final class DeserializeOnlyAutoValueTest { private final Moshi moshi = new Moshi.Builder() .add(DeserializeOnly.ADAPTER_FACTORY) .add(DataFactories.create()) .build(); @Test public void serialize() { final String json = AutoValueClass.jsonAdapter(moshi) .toJson(new AutoValue_DeserializeOnlyAutoValueTest_AutoValueClass(1, 2)); assertThat(json).isEqualTo("{\"foo\":1}"); } @Test public void deserialize() throws IOException { final AutoValueClass autoValueClass = AutoValueClass.jsonAdapter(moshi).fromJson("{\"foo\": 1,\"bar\": 2}"); assertThat(autoValueClass.foo()).isEqualTo(1); assertThat(autoValueClass.bar()).isEqualTo(2); } @AutoValue abstract static class AutoValueClass { public static JsonAdapter jsonAdapter(Moshi moshi) { return new AutoValue_DeserializeOnlyAutoValueTest_AutoValueClass.MoshiJsonAdapter(moshi); } abstract Integer foo(); @DeserializeOnly abstract Integer bar(); } } ================================================ FILE: src/integrationTest/java/com/serjltt/moshi/adapters/LazyAdaptersAutoValueTest.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import java.util.Collections; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; /** Test all lazy adapters to work in integration with AutoValue extensions. */ public class LazyAdaptersAutoValueTest { private final Moshi moshi = new Moshi.Builder() .add(Wrapped.ADAPTER_FACTORY) .add(DataFactories.create()) .build(); @Test public void unwrap() throws Exception { JsonAdapter adapter = moshi.adapter(Data.class); Data fromJson = adapter.fromJson("{\n" + " \"name\": \"data_name\",\n" + " \"meta\": {\n" + " \"1\": {\n" + " \"value1\": \"value1\",\n" + " \"value2\": 2\n" + " }\n" + " }\n" + "}"); assertThat(fromJson.name()).isEqualTo("data_name"); assertThat(fromJson.meta().value1).isEqualTo("value1"); assertThat(fromJson.meta().value2).isEqualTo(2); } @Test public void wrap() throws Exception { JsonAdapter adapter = moshi.adapter(Data.class, Collections.singleton(Wrapped.Factory.create("foo"))); Data.Meta meta = new Data.Meta(); meta.value1 = "value1"; meta.value2 = 2; Data data = new AutoValue_Data("data_name", meta); String toJson = adapter.toJson(data); assertThat(toJson).isEqualTo( "{\"foo\":{\"name\":\"data_name\"," + "\"meta\":{\"1\":{\"value1\":\"value1\",\"value2\":2}}}}"); } } ================================================ FILE: src/integrationTest/java/com/serjltt/moshi/adapters/LazyAdaptersRetrofitTest.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.Json; import com.squareup.moshi.Moshi; import java.io.IOException; import okhttp3.ResponseBody; import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockWebServer; import okhttp3.mockwebserver.RecordedRequest; import org.junit.Rule; import org.junit.Test; import retrofit2.Call; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.moshi.MoshiConverterFactory; import retrofit2.http.Body; import retrofit2.http.GET; import retrofit2.http.POST; import static org.assertj.core.api.Assertions.assertThat; /** Test all lazy adapters to work in integration with retrofit. */ public final class LazyAdaptersRetrofitTest { @Rule public final MockWebServer server = new MockWebServer(); private final Moshi moshi = new Moshi.Builder() .add(Wrapped.ADAPTER_FACTORY) .add(FirstElement.ADAPTER_FACTORY) .add(ElementAt.ADAPTER_FACTORY) .build(); private final Retrofit retrofit = new Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create(moshi)) .baseUrl(server.url("/")) .build(); private final Service service = retrofit.create(Service.class); @Test public void unwrapJsonAdapter() throws Exception { assertResponse(service.unwrap(), "{\n" + " \"one\": {\n" + " \"two\": \"works!\"\n" + " }\n" + "}", "works!"); } @Test public void unwrapNestedJsonAdapter() throws Exception { server.enqueue(new MockResponse().setBody("{\n" + " \"one\": {\n" + " \"two\": {\n" + " \"item\": {\n" + " \"foo\": \"this\"\n" + " },\n" + " \"item2\": {\n" + " \"bar\": 1234\n" + " },\n" + " \"foobar\": 567\n" + " }\n" + " }\n" + "}")); Response response = service.unwrapNested().execute(); assertThat(response.body().foobar).isEqualTo(567); assertThat(response.body().foo).isEqualTo("this"); assertThat(response.body().bar).isEqualTo(1234); } @Test public void wrapPostBody() throws Exception { server.enqueue(new MockResponse()); Call call = service.wrappedPost("one"); call.execute(); RecordedRequest recorded = server.takeRequest(); assertThat(recorded.getBody() .readUtf8()) .isEqualTo("{\"1\":{\"2\":\"one\"}}"); } @Test public void firstElementJsonAdapter() throws Exception { assertResponse(service.firstElement(), "[\n" + " \"expected\",\n" + " \"ignored\"\n" + "]", "expected"); } @Test public void elementAtJsonAdapter() throws Exception { assertResponse(service.elementAt(), "[\n" + " \"one\",\n" + " \"two\",\n" + " \"three\"\n" + "]", "three"); } @Test public void unwrapFirstElement() throws Exception { assertResponse(service.unwrapFirstElement(), "{\n" + " \"one\": {\n" + " \"two\": [\n" + " \"first\"\n" + " ]\n" + " }\n" + "}", "first"); } private void assertResponse(Call call, String input, T expected) throws IOException { server.enqueue(new MockResponse().setBody(input)); Response response = call.execute(); assertThat(response.body()).isEqualTo(expected); } /** Test service for all lazy adapters. */ private interface Service { /** Helps to test the unwrap adapter. */ @GET("/") @Wrapped(path = {"one", "two"}) Call unwrap(); @GET("/") @Wrapped(path = {"one", "two"}) Call unwrapNested(); @POST("/") Call wrappedPost(@Body @Wrapped(path = {"1", "2"}) String value); /** Helps to test the first element json adapter. */ @GET("/") @FirstElement Call firstElement(); /** Helps to test the first element json adapter. */ @GET("/") @Wrapped(path = {"one", "two"}) @FirstElement Call unwrapFirstElement(); @GET("/") @ElementAt(index = 2) Call elementAt(); } static class Nested { @Json(name = "item") @Wrapped(path = "foo") String foo; @Json(name = "item2") @Wrapped(path = "bar") int bar; int foobar; } } ================================================ FILE: src/integrationTest/java/com/serjltt/moshi/adapters/LazyAdaptersRxJavaTest.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.Moshi; import io.reactivex.Flowable; import io.reactivex.Maybe; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.observers.BaseTestConsumer; import java.lang.annotation.Annotation; import java.util.Collections; import java.util.Set; import java.util.concurrent.Callable; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; /** Tests if specific adapters behave properly with RxJava2. */ @RunWith(Parameterized.class) public final class LazyAdaptersRxJavaTest { private static final Moshi MOSHI = new Moshi.Builder() .add(Wrapped.ADAPTER_FACTORY) .build(); private static Callable failingCallable() { Set annotations = Collections.singleton(Wrapped.Factory.create(true, "one", "two", "three")); final JsonAdapter adapter = MOSHI.adapter(String.class, annotations); return new Callable() { @Override public String call() throws Exception { return adapter.fromJson("{\n" + " \"one\": {\n" + " \"two\": null\n" + " }\n" + "}"); } }; } @Parameterized.Parameters public static BaseTestConsumer[] testData() { return new BaseTestConsumer[] { Single.fromCallable(failingCallable()).test(), Observable.fromCallable(failingCallable()).test(), Maybe.fromCallable(failingCallable()).test(), Flowable.fromCallable(failingCallable()).test() }; } private final BaseTestConsumer testConsumer; public LazyAdaptersRxJavaTest(BaseTestConsumer testConsumer) { this.testConsumer = testConsumer; } @Test public void reactiveTypeYieldsAppropriateError() throws Exception { //noinspection unchecked testConsumer .assertFailureAndMessage(JsonDataException.class, "Wrapped Json expected at path: [one, two, three]. Found null at $.one.two"); } } ================================================ FILE: src/integrationTest/java/com/serjltt/moshi/adapters/Nullable.java ================================================ package com.serjltt.moshi.adapters; /** For testing purposes. */ public @interface Nullable { } ================================================ FILE: src/integrationTest/java/com/serjltt/moshi/adapters/SerializeOnlyAutoValueTest.java ================================================ package com.serjltt.moshi.adapters; import com.google.auto.value.AutoValue; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import java.io.IOException; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public final class SerializeOnlyAutoValueTest { private final Moshi moshi = new Moshi.Builder() .add(SerializeOnly.ADAPTER_FACTORY) .add(DataFactories.create()) .build(); @Test public void serialize() { final String json = AutoValueClass.jsonAdapter(moshi) .toJson(new AutoValue_SerializeOnlyAutoValueTest_AutoValueClass(1, 2)); assertThat(json).isEqualTo("{\"foo\":1,\"bar\":2}"); } @Test public void deserialize() throws IOException { final AutoValueClass autoValueClass = AutoValueClass.jsonAdapter(moshi).fromJson("{\"foo\": 1,\"bar\": 2}"); assertThat(autoValueClass.foo()).isEqualTo(1); assertThat(autoValueClass.bar()).isNull(); } @AutoValue abstract static class AutoValueClass { public static JsonAdapter jsonAdapter(Moshi moshi) { return new AutoValue_SerializeOnlyAutoValueTest_AutoValueClass.MoshiJsonAdapter(moshi); } abstract Integer foo(); @SerializeOnly @Nullable abstract Integer bar(); } } ================================================ FILE: src/integrationTest/java/com/serjltt/moshi/adapters/TransientAutoValueTest.java ================================================ package com.serjltt.moshi.adapters; import com.google.auto.value.AutoValue; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import java.io.IOException; import java.lang.annotation.Annotation; import java.util.Collections; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public final class TransientAutoValueTest { private final Moshi moshi = new Moshi.Builder() .add(Transient.ADAPTER_FACTORY) .add(DataFactories.create()) .build(); @Test public void serialize() { final String json = AutoValueClass.jsonAdapter(moshi) .toJson(new AutoValue_TransientAutoValueTest_AutoValueClass(1, 2)); assertThat(json).isEqualTo("{\"foo\":1}"); } @Test public void deserialize() throws IOException { final AutoValueClass autoValueClass = AutoValueClass.jsonAdapter(moshi).fromJson("{\"foo\": 1,\"bar\": 2}"); assertThat(autoValueClass.foo()).isEqualTo(1); assertThat(autoValueClass.bar()).isNull(); } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(String.class, Collections.singleton(new SerializeOnly() { @Override public Class annotationType() { return Transient.class; } })); assertThat(adapter.toString()).isEqualTo("JsonAdapter(String).nullSafe().transient()"); } @AutoValue abstract static class AutoValueClass { public static JsonAdapter jsonAdapter(Moshi moshi) { return new AutoValue_TransientAutoValueTest_AutoValueClass.MoshiJsonAdapter(moshi); } abstract Integer foo(); @Transient @Nullable abstract Integer bar(); } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/DefaultOnDataMismatchAdapter.java ================================================ /* * Copyright (C) 2017 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Set; /** * Adapter that fallbacks to a default value in case there's a mismatch. */ public final class DefaultOnDataMismatchAdapter extends JsonAdapter { private final JsonAdapter delegate; private final T defaultValue; DefaultOnDataMismatchAdapter(JsonAdapter delegate, T defaultValue) { this.delegate = delegate; this.defaultValue = defaultValue; } @Override public T fromJson(JsonReader reader) throws IOException { Object jsonValue = reader.readJsonValue(); try { return delegate.fromJsonValue(jsonValue); } catch (JsonDataException ignore) { return defaultValue; } } @Override public void toJson(JsonWriter writer, T value) throws IOException { delegate.toJson(writer, value); } @Override public String toString() { return delegate + ".defaultOnDatMisMatch(" + defaultValue + ')'; } /** Builds an adapter that fallbacks to a default value in case there's a mismatch. */ public static JsonAdapter.Factory newFactory(final Type type, final T defaultValue) { return new Factory() { @Override public JsonAdapter create(Type requestedType, Set annotations, Moshi moshi) { if (Types.equals(type, requestedType)) { JsonAdapter delegate = moshi.nextAdapter(this, type, annotations); return new DefaultOnDataMismatchAdapter<>(delegate, defaultValue); } return null; } }; } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/DeserializeOnly.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Set; /** * Indicates that the annotated field may only be deserialized. * *

To leverage from {@link DeserializeOnly} {@link DeserializeOnly#ADAPTER_FACTORY} must be * added to your {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(DeserializeOnly.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD }) public @interface DeserializeOnly { /** Builds an adapter that can process a types annotated with {@link DeserializeOnly}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Set nextAnnotations = Types.nextAnnotations(annotations, DeserializeOnly.class); if (nextAnnotations == null) return null; return new TransientJsonAdapter<>(moshi.adapter(type, nextAnnotations), false, true); } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/ElementAt.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Set; import static com.serjltt.moshi.adapters.Util.nextAnnotations; /** * Indicates that the annotated type/field is expected to be the element * at the given index of a json array. * *

For example if a json object is: *

 *   [
 *    {
 *      "some_field": "some_value",
 *      "other_field": "other_value"
 *    },
 *    {
 *      "some_field": "some_value_2",
 *      "other_field": "other_value_2"
 *    }
 *   ]
 * 
* And the consumer only cares about the second element, if using retrofit a service method would * look like: * *

 *   {@literal @}GET("path/")
 *   {@literal @}ElementAt(index = 1) Call<DataObject> getData();
 * 
* * The resulting response returned by {@code response.body()} will be an instance of {@code * DataObject} with the respective values set. * *

To leverage from {@link ElementAt} {@link ElementAt#ADAPTER_FACTORY} * must be added to your {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(ElementAt.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) public @interface ElementAt { /** * Represents the index location at which the element will be expected to be. * If the size of the array will be less then the provided index, * the companion adapter will return {@code null}. */ int index(); /** Builds an adapter that can process a types annotated with {@link ElementAt}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Pair> nextAnnotations = nextAnnotations(annotations, ElementAt.class); if (nextAnnotations == null || !nextAnnotations.second.isEmpty()) return null; return new ElementAtJsonAdapter<>(type, moshi, nextAnnotations.first.index()); } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/ElementAtJsonAdapter.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.io.IOException; import java.lang.reflect.Type; import java.util.Collections; import java.util.List; /** * {@linkplain JsonAdapter} that extracts the element at the given index * of an array of (a field) type annotated with {@linkplain ElementAt}. */ final class ElementAtJsonAdapter extends JsonAdapter { private final JsonAdapter> delegate; private final int index; ElementAtJsonAdapter(Type type, Moshi moshi, int index) { Type listType = Types.newParameterizedType(List.class, type); delegate = moshi.adapter(listType); this.index = index; } @Override public T fromJson(JsonReader reader) throws IOException { List fromJson = delegate.fromJson(reader); if (fromJson != null && index < fromJson.size()) return fromJson.get(index); return null; } @Override public void toJson(JsonWriter writer, T value) throws IOException { delegate.toJson(writer, Collections.singletonList(value)); } @Override public String toString() { return delegate + ".elementAt(" + index + ")"; } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/FallbackEnum.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Set; /** * Indicates that the annotated enum has a fallback value. The fallback must be set via * {@link #name()}. If no enum constant with the provided name is declared in the annotated * enum type an {@linkplain AssertionError assertion error} will be thrown. * *

To leverage from {@link FallbackEnum} {@link FallbackEnum#ADAPTER_FACTORY} must be added to * your {@linkplain Moshi moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(FallbackEnum.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface FallbackEnum { String name(); /** Builds an adapter that can process enums annotated with {@link FallbackEnum}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { if (!annotations.isEmpty()) return null; Class rawType = Types.getRawType(type); if (rawType.isEnum()) { FallbackEnum annotation = rawType.getAnnotation(FallbackEnum.class); if (annotation == null) return null; //noinspection unchecked return new FallbackEnumJsonAdapter<>((Class) rawType, annotation.name()) .nullSafe(); } return null; } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/FallbackEnumJsonAdapter.java ================================================ /* * Copyright 2016 Serj Lotutovici * Copyright (C) 2014 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.Json; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import java.io.IOException; /** * {@linkplain JsonAdapter} that fallbacks to a default enum constant declared in the enum type * annotated with {@linkplain FallbackEnum}. */ final class FallbackEnumJsonAdapter> extends JsonAdapter { private final Class enumType; private final String[] nameStrings; private final T[] constants; private final JsonReader.Options options; private final T fallbackConstant; FallbackEnumJsonAdapter(Class enumType, String fallback) { fallbackConstant = Enum.valueOf(enumType, fallback); this.enumType = enumType; try { constants = enumType.getEnumConstants(); nameStrings = new String[constants.length]; for (int i = 0; i < constants.length; i++) { T constant = constants[i]; Json annotation = enumType.getField(constant.name()).getAnnotation(Json.class); String name = annotation != null ? annotation.name() : constant.name(); nameStrings[i] = name; } options = JsonReader.Options.of(nameStrings); } catch (NoSuchFieldException e) { throw new AssertionError(e); } } @Override public T fromJson(JsonReader reader) throws IOException { int index = reader.selectString(options); if (index != -1) return constants[index]; reader.nextString(); return fallbackConstant; } @Override public void toJson(JsonWriter writer, T value) throws IOException { writer.value(nameStrings[value.ordinal()]); } @Override public String toString() { return "JsonAdapter(" + enumType.getName() + ").fallbackEnum(" + fallbackConstant + ")"; } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/FallbackOnNull.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Set; import java.util.Locale; import static com.serjltt.moshi.adapters.Util.nextAnnotations; /** * Indicates that the annotated field may be {@code null} in the json source and thus requires a * fallback value. * *

To leverage from {@linkplain FallbackOnNull} {@linkplain FallbackOnNull#ADAPTER_FACTORY} * must be added to your {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(FallbackOnNull.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.ANNOTATION_TYPE }) public @interface FallbackOnNull { /** Fallback value for {@code boolean} primitives. Default: {@code false}. */ boolean fallbackBoolean() default false; /** Fallback value for {@code byte} primitives. Default: {@code Byte.MIN_VALUE}. */ byte fallbackByte() default Byte.MIN_VALUE; /** Fallback value for {@code char} primitives. Default: {@code Character.MIN_VALUE}. */ char fallbackChar() default Character.MIN_VALUE; /** Fallback value for {@code double} primitives. Default: {@code Double.MIN_VALUE}. */ double fallbackDouble() default Double.MIN_VALUE; /** Fallback value for {@code float} primitives. Default: {@code Float.MIN_VALUE}. */ float fallbackFloat() default Float.MIN_VALUE; /** Fallback value for {@code int} primitives. Default: {@code Integer.MIN_VALUE}. */ int fallbackInt() default Integer.MIN_VALUE; /** Fallback value for {@code long} primitives. Default: {@code Long.MIN_VALUE}. */ long fallbackLong() default Long.MIN_VALUE; /** Fallback value for {@code short} primitives. Default: {@code Short.MIN_VALUE}. */ short fallbackShort() default Short.MIN_VALUE; /** Builds an adapter that can process a types annotated with {@link FallbackOnNull}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Pair> nextAnnotations = nextAnnotations(annotations, FallbackOnNull.class); if (nextAnnotations == null) return null; Class rawType = Types.getRawType(type); if (!FallbackOnNullJsonAdapter.PRIMITIVE_CLASSES.contains(rawType)) return null; String fallbackType = fallbackType(rawType); Object fallback = retrieveFallback(nextAnnotations.first, fallbackType); return new FallbackOnNullJsonAdapter<>(moshi.adapter(type, nextAnnotations.second), fallback, fallbackType); } /** Invokes the appropriate fallback method based on the {@code fallbackType}. */ private Object retrieveFallback(FallbackOnNull annotation, String fallbackType) { try { Method fallbackMethod = FallbackOnNull.class.getMethod(fallbackType); return fallbackMethod.invoke(annotation); } catch (Exception e) { throw new AssertionError(e); } } /** Constructs the appropriate fallback method name based on the {@code rawType}. */ private String fallbackType(Class rawType) { String typeName = rawType.getSimpleName(); String methodSuffix = typeName.substring(0, 1).toUpperCase(Locale.US) + typeName.substring(1); return "fallback" + methodSuffix; } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/FallbackOnNullJsonAdapter.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import java.io.IOException; import java.util.LinkedHashSet; import java.util.Set; /** * {@linkplain JsonAdapter} that fallbacks to a default value of a primitive field annotated with * {@linkplain FallbackOnNull}. */ final class FallbackOnNullJsonAdapter extends JsonAdapter { /** Set of primitives classes that are supported by this adapter. */ static final Set> PRIMITIVE_CLASSES = new LinkedHashSet<>(); static { PRIMITIVE_CLASSES.add(boolean.class); PRIMITIVE_CLASSES.add(byte.class); PRIMITIVE_CLASSES.add(char.class); PRIMITIVE_CLASSES.add(double.class); PRIMITIVE_CLASSES.add(float.class); PRIMITIVE_CLASSES.add(int.class); PRIMITIVE_CLASSES.add(long.class); PRIMITIVE_CLASSES.add(short.class); } final JsonAdapter delegate; final T fallback; final String fallbackType; FallbackOnNullJsonAdapter(JsonAdapter delegate, T fallback, String fallbackType) { this.delegate = delegate; this.fallback = fallback; this.fallbackType = fallbackType; } @Override public T fromJson(JsonReader reader) throws IOException { if (reader.peek() == JsonReader.Token.NULL) { reader.nextNull(); // We need to consume the value. return fallback; } return delegate.fromJson(reader); } @Override public void toJson(JsonWriter writer, T value) throws IOException { delegate.toJson(writer, value); } @Override public String toString() { return delegate + ".fallbackOnNull(" + fallbackType + '=' + fallback + ')'; } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/FilterNulls.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Collection; import java.util.Set; /** * Indicates that the annotated field may not contain any null values. * This annotation is applicable to all Collections. * *

To leverage from {@link FilterNulls} {@link FilterNulls#ADAPTER_FACTORY} must be * added to your {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(FilterNulls.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD }) public @interface FilterNulls { /** Builds an adapter that can process types annotated with {@link FilterNulls}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Set nextAnnotations = Types.nextAnnotations(annotations, FilterNulls.class); if (nextAnnotations == null || !nextAnnotations.isEmpty()) return null; Class rawType = Types.getRawType(type); if (Collection.class.isAssignableFrom(rawType)) { return new FilterNullsJsonAdapter<>(moshi.adapter(type, nextAnnotations)); } return null; } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/FilterNullsJsonAdapter.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import java.io.IOException; import java.util.Collection; import java.util.Iterator; /** * {@link JsonAdapter} that filters null values out. */ final class FilterNullsJsonAdapter extends JsonAdapter { private final JsonAdapter delegate; FilterNullsJsonAdapter(JsonAdapter delegate) { this.delegate = delegate; } @Override public T fromJson(JsonReader reader) throws IOException { return removeNulls(delegate.fromJson(reader)); } @Override public void toJson(JsonWriter writer, T value) throws IOException { delegate.toJson(writer, removeNulls(value)); } @Override public String toString() { return delegate + ".filterNulls()"; } private T removeNulls(final T value) { if (value != null) { final Iterator it = ((Collection) value).iterator(); while (it.hasNext()) { if (it.next() == null) { it.remove(); } } } return value; } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/FirstElement.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Set; /** * Indicates that the annotated type/field is expected to be the first element of a * json array. * *

For example if a json object is returned as: *

 *   [
 *    {
 *      "some_field": "some_value",
 *      "other_field": "other_value"
 *    }
 *   ]
 * 
* And the consumer only cares about the actual element, in the case of using a retrofit service * method the code would look like: * *

 *   {@literal @}GET("path/")
 *   {@literal @}FirstElement Call<DataObject> getData();
 * 
* * The resulting response returned by {@code response.body()} will be an instance of {@code * DataObject} with the respective values set. * *

To leverage from {@link FirstElement} {@linkplain FirstElement#ADAPTER_FACTORY} * must be added to a {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(FirstElement.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD }) public @interface FirstElement { /** Builds an adapter that can process a types annotated with {@link FirstElement}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Set nextAnnotations = Types.nextAnnotations(annotations, FirstElement.class); if (nextAnnotations == null || !nextAnnotations.isEmpty()) return null; return new ElementAtJsonAdapter<>(type, moshi, 0); } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/LastElement.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Set; /** * Indicates that the annotated type/field is expected to be the last element of a * json array. * *

For example if a json object is returned as: *

 *   [
 *    {
 *      "some_field": "some_value",
 *      "other_field": "other_value"
 *    }
 *   ]
 * 
* And the consumer only cares about the last element, in the case of using a retrofit service * method the code would look like: * *

 *   {@literal @}GET("path/")
 *   {@literal @}LastElement Call<DataObject> getData();
 * 
* * The resulting response returned by {@code response.body()} will be an instance of {@code * DataObject} with the respective values set. * *

To leverage from {@link LastElement} {@linkplain LastElement#ADAPTER_FACTORY} * must be added to a {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(LastElement.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD }) public @interface LastElement { /** Builds an adapter that can process a type/field annotated with {@link LastElement}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Set nextAnnotations = Types.nextAnnotations(annotations, LastElement.class); if (nextAnnotations == null || !nextAnnotations.isEmpty()) return null; return new LastElementJsonAdapter<>(type, moshi); } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/LastElementJsonAdapter.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.io.IOException; import java.lang.reflect.Type; import java.util.Collections; import java.util.List; /** * {@linkplain JsonAdapter} that extracts the last element * of an array of (a field) type or a method annotated with {@linkplain LastElement}. */ final class LastElementJsonAdapter extends JsonAdapter { private final JsonAdapter> delegate; LastElementJsonAdapter(Type type, Moshi moshi) { Type listType = Types.newParameterizedType(List.class, type); delegate = moshi.adapter(listType); } @Override public T fromJson(JsonReader reader) throws IOException { List fromJson = delegate.fromJson(reader); if (fromJson != null && !fromJson.isEmpty()) return fromJson.get(fromJson.size() - 1); return null; } @Override public void toJson(JsonWriter writer, T value) throws IOException { delegate.toJson(writer, Collections.singletonList(value)); } @Override public String toString() { return delegate + ".lastElement()"; } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/Pair.java ================================================ package com.serjltt.moshi.adapters; /** A simple pair data class. */ final class Pair { final F first; final S second; Pair(F first, S second) { this.first = first; this.second = second; } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/SerializeNulls.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Set; /** * Indicates that the annotated type/field should be serialized as {@code null} in case of a * empty/null value. * *

To leverage from {@link SerializeNulls} {@link SerializeNulls#ADAPTER_FACTORY} * must be added to a {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(SerializeNulls.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD }) public @interface SerializeNulls { /** Builds an adapter that can process a types annotated with {@link SerializeNulls}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Set nextAnnotations = Types.nextAnnotations(annotations, SerializeNulls.class); if (nextAnnotations == null) return null; return moshi.adapter(type, nextAnnotations).serializeNulls(); } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/SerializeOnly.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Set; /** * Indicates that the annotated field may only be serialized. * *

To leverage from {@link SerializeOnly} {@link SerializeOnly#ADAPTER_FACTORY} must be added * to your {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(SerializeOnly.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD }) public @interface SerializeOnly { /** Builds an adapter that can process a types annotated with {@link SerializeOnly}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Set nextAnnotations = Types.nextAnnotations(annotations, SerializeOnly.class); if (nextAnnotations == null) return null; return new TransientJsonAdapter<>(moshi.adapter(type, nextAnnotations), true, false); } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/SerializeOnlyNonEmpty.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Collection; import java.util.Map; import java.util.Set; /** * Indicates that the annotated type/field should not be serialized in case of an * empty value. * *

To leverage from {@link SerializeOnlyNonEmpty} {@link SerializeOnlyNonEmpty#ADAPTER_FACTORY} * must be added to your {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(SerializeOnlyNonEmpty.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD }) public @interface SerializeOnlyNonEmpty { /** Builds an adapter that can process a types annotated with {@link SerializeOnlyNonEmpty}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Set nextAnnotations = Types.nextAnnotations(annotations, SerializeOnlyNonEmpty.class); if (nextAnnotations == null) return null; Class rawType = Types.getRawType(type); if (rawType.isArray() || Collection.class.isAssignableFrom(rawType) || Map.class.isAssignableFrom(rawType)) { return new SerializeOnlyNonEmptyJsonAdapter<>(moshi.adapter(type, nextAnnotations)); } return null; } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/SerializeOnlyNonEmptyJsonAdapter.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import java.io.IOException; import java.lang.reflect.Array; import java.util.Collection; import java.util.Map; /** * {@linkplain JsonAdapter} that will not serialize {@code T} when the passed value is empty. */ final class SerializeOnlyNonEmptyJsonAdapter extends JsonAdapter { private final JsonAdapter delegate; SerializeOnlyNonEmptyJsonAdapter(JsonAdapter delegate) { this.delegate = delegate; } @Override public T fromJson(JsonReader reader) throws IOException { return delegate.fromJson(reader); } @Override public void toJson(JsonWriter writer, T value) throws IOException { if (isNotEmpty(value)) { delegate.toJson(writer, value); } else { // We'll need to consume this property otherwise we'll get an IllegalArgumentException. delegate.toJson(writer, null); } } private boolean isNotEmpty(final T value) { if (value instanceof Collection) { Collection collection = (Collection) value; return collection.size() > 0; } else if (value instanceof Map) { Map map = (Map) value; return map.size() > 0; } else if (value != null) { return Array.getLength(value) > 0; } return false; } @Override public String toString() { return delegate + ".serializeOnlyNonEmpty()"; } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/Transient.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Set; /** * Indicates that the annotated method is transient. * *

To leverage from {@link Transient} {@link Transient#ADAPTER_FACTORY} must be added * to your {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(Transient.ADAPTER_FACTORY)
 *      .build();
 * 
*/ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) public @interface Transient { /** Builds an adapter that can process a type annotated with {@link Transient}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Set nextAnnotations = Types.nextAnnotations(annotations, Transient.class); if (nextAnnotations == null) return null; return new TransientJsonAdapter<>(moshi.adapter(type, nextAnnotations), false, false); } }; } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/TransientJsonAdapter.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import java.io.IOException; /** * {@link JsonAdapter} with transient functionality. The consumer can decide to ether serialize or * deserialize, or make the adapter completely transient. */ final class TransientJsonAdapter extends JsonAdapter { private final JsonAdapter delegate; private final boolean serialize; private final boolean deserialize; TransientJsonAdapter(JsonAdapter delegate, boolean serialize, boolean deserialize) { this.delegate = delegate; this.serialize = serialize; this.deserialize = deserialize; } @Override public T fromJson(JsonReader reader) throws IOException { if (deserialize) { return delegate.fromJson(reader); } else { reader.skipValue(); return null; } } @Override public void toJson(JsonWriter writer, T value) throws IOException { if (serialize) { delegate.toJson(writer, value); } else { // We'll need to consume this property otherwise we'll get an IllegalArgumentException. delegate.toJson(writer, null); } } @Override public String toString() { return delegate + ((serialize && deserialize) ? "" : serialize ? ".serializeOnly()" : deserialize ? ".deserializeOnly()" : ".transient()"); } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/Util.java ================================================ /* * Copyright 2014 Square, Inc. * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonQualifier; import java.lang.annotation.Annotation; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; final class Util { /** * Checks if {@code annotations} contains {@code jsonQualifier}. * Returns a pair containing the subset of {@code annotations} without {@code jsonQualifier} * and the {@code jsonQualified} instance, or null if {@code annotations} does not contain * {@code jsonQualifier}. */ public static Pair> nextAnnotations( Set annotations, Class jsonQualifier) { if (!jsonQualifier.isAnnotationPresent(JsonQualifier.class)) { throw new IllegalArgumentException(jsonQualifier + " is not a JsonQualifier."); } if (annotations.isEmpty()) { return null; } for (Annotation annotation : annotations) { if (jsonQualifier.equals(annotation.annotationType())) { Set delegateAnnotations = new LinkedHashSet<>(annotations); delegateAnnotations.remove(annotation); //noinspection unchecked Protected by the if statment. return new Pair<>((A) annotation, Collections.unmodifiableSet(delegateAnnotations)); } A delegate = findDelegatedAnnotation(annotation, jsonQualifier); if (delegate != null) { Set delegateAnnotations = new LinkedHashSet<>(annotations); delegateAnnotations.remove(annotation); return new Pair<>(delegate, Collections.unmodifiableSet(delegateAnnotations)); } } return null; } private static A findDelegatedAnnotation( Annotation annotation, Class jsonQualifier) { for (Annotation delegatedAnnotation : annotation.annotationType().getAnnotations()) { if (jsonQualifier.equals(delegatedAnnotation.annotationType())) { //noinspection unchecked return (A) delegatedAnnotation; } } return null; } private Util() { throw new AssertionError("No instances."); } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/Wrapped.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import java.lang.annotation.Annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Arrays; import java.util.Set; import static com.serjltt.moshi.adapters.Util.nextAnnotations; /** * Indicates that the annotated type/field should be unwrapped from the provided {@code path}. * *

For example if a json object is: *

 *   {
 *     "response": {
 *       "status": "OK"
 *     }
 *   }
 * 
* And the consumer only cares about the value of {@code status}, if using retrofit, a service * method would look like: * *

 *   {@literal @}GET("path/")
 *   {@literal @}Wrapped({"response", "status"}) Call<String> getStatus();
 * 
* * The resulting response returned by {@code response.body()} will be a {@code String} with the * value {@code "OK"}. * *

To leverage from {@link Wrapped} {@link Wrapped#ADAPTER_FACTORY} must be * added to your {@linkplain Moshi Moshi instance}: * *


 *   Moshi moshi = new Moshi.Builder()
 *      .add(Wrapped.ADAPTER_FACTORY)
 *      .build();
 * 
* * DISCLAIMER: The order of {@linkplain JsonAdapter added json adapters} matters, to ensure * {@linkplain Wrapped correct un-wrapping} behaviour the adapter factory must be the * first custom adapter added to the {@link Moshi.Builder}. */ @Documented @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE }) public @interface Wrapped { /** The path to the wrapped json path. */ String[] path(); /** * Indicates if the adapter should fail when the json object was not found at the indicated path. * Default {@code true}. */ boolean failOnNotFound() default true; /** Builds an adapter that can process a types annotated with {@link Wrapped}. */ JsonAdapter.Factory ADAPTER_FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Pair> nextAnnotations = nextAnnotations(annotations, Wrapped.class); if (nextAnnotations == null) return null; JsonAdapter adapter = moshi.adapter(type, nextAnnotations.second); Wrapped wrapped = nextAnnotations.first; return new WrappedJsonAdapter<>(adapter, wrapped.path(), wrapped.failOnNotFound()); } }; /** Allows to easily create a new instance of {@link Wrapped} annotation. */ final class Factory { /** Create a new instance of {@link Wrapped} with the specified JSON path. */ public static Wrapped create(final String... path) { return create(true, path); } /** Create a new instance of {@link Wrapped} with the specified JSON path. */ public static Wrapped create(final boolean failOnNotFound, final String... path) { return new Wrapped() { @Override public Class annotationType() { return Wrapped.class; } @Override public String[] path() { return path; } @Override public boolean failOnNotFound() { return failOnNotFound; } @Override public int hashCode() { int result = Arrays.hashCode(path); result = 43 * result + (failOnNotFound ? 1 : 0); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Wrapped wrapped = (Wrapped) obj; return Arrays.equals(path, wrapped.path()) && failOnNotFound == wrapped.failOnNotFound(); } @Override public String toString() { return "Wrapped(" + "path=" + Arrays.asList(path) + ", failOnNotFound=" + failOnNotFound + ")"; } }; } private Factory() { throw new AssertionError("No instances."); } } } ================================================ FILE: src/main/java/com/serjltt/moshi/adapters/WrappedJsonAdapter.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import java.io.IOException; import java.util.Arrays; /** {@linkplain JsonAdapter} that unwraps the type/field annotated with {@linkplain Wrapped}. */ final class WrappedJsonAdapter extends JsonAdapter { private final JsonAdapter delegate; private final String[] path; private final boolean failOnNotFound; WrappedJsonAdapter(JsonAdapter delegate, String[] path, boolean failOnNotFound) { this.delegate = delegate; this.path = path; this.failOnNotFound = failOnNotFound; } @Override public T fromJson(JsonReader reader) throws IOException { return fromJson(delegate, reader, path, 0, failOnNotFound); } @Override public void toJson(JsonWriter writer, T value) throws IOException { toJson(delegate, writer, value, path, 0); } @Override public String toString() { return delegate + String.format(".wrapped(%s)", Arrays.asList(path)) + (failOnNotFound ? ".failOnNotFound()" : ""); } /** * Recursively goes through the json and finds the given root. Returns the object wrapped by the * provided {@code path}. */ private static T fromJson(JsonAdapter adapter, JsonReader reader, String[] path, int index, boolean failOnNotFound) throws IOException { if (index == path.length) { //noinspection unchecked This puts full responsibility on the caller. return adapter.fromJson(reader); } else { reader.beginObject(); Exception caughtException = null; try { String root = path[index]; while (reader.hasNext()) { if (reader.nextName().equals(root)) { if (reader.peek() == JsonReader.Token.NULL) { // Consumer expects a value, not a null. if (failOnNotFound) { throw new JsonDataException(String.format( "Wrapped Json expected at path: %s. Found null at %s", Arrays.asList(path), reader.getPath() )); } return reader.nextNull(); } return fromJson(adapter, reader, path, ++index, failOnNotFound); } else { reader.skipValue(); } } } catch (Exception e) { caughtException = e; } finally { // If the try block throw an exception, rethrow it up the stack. if (caughtException instanceof IOException) { //noinspection ThrowFromFinallyBlock throw (IOException) caughtException; } else if (caughtException instanceof JsonDataException) { //noinspection ThrowFromFinallyBlock throw (JsonDataException) caughtException; } else if (caughtException != null) { //noinspection ThrowFromFinallyBlock throw new AssertionError(caughtException); } // If the json has an additional key, that was not red, we ignore it. while (reader.hasNext()) { reader.skipValue(); } // End object, so that other adapters (if any) can proceed. reader.endObject(); } throw new JsonDataException(String.format( "Wrapped Json expected at path: %s. Actual: %s", Arrays.asList(path), reader.getPath())); } } /** * Recursively writes the respective roots forming a json object that resembles the {@code path} * wrapping the type of the {@code adapter}. */ private static void toJson(JsonAdapter adapter, JsonWriter writer, T value, String[] path, int index) throws IOException { if (value != null || writer.getSerializeNulls()) { if (index == path.length) { adapter.toJson(writer, value); } else { writer.beginObject(); writer.name(path[index]); toJson(adapter, writer, value, path, ++index); writer.endObject(); } } else { // If we don't propagate the null value the writer will throw. writer.nullValue(); } } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/Custom.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.FromJson; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.ToJson; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * Helper annotation (to test if JsonQualifier annotations are propagated further down the stream). */ @JsonQualifier @Retention(RetentionPolicy.RUNTIME) @interface Custom { /** String adapter, that will append "Custom" on read, and exclude it on write. */ final class CustomAdapter { @Custom @FromJson String fromJson(String str) { return str + "Custom"; } @ToJson String toJson(@Custom String str) { return str.substring(0, str.length() - "Custom".length()); } } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/DefaultOnDataMismatchAdapterTest.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.Json; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Collections; import java.util.List; import java.util.Set; import org.junit.Test; import static com.serjltt.moshi.adapters.DefaultOnDataMismatchAdapterTest.Fruit.APPLE; import static com.serjltt.moshi.adapters.DefaultOnDataMismatchAdapterTest.Fruit.BANANA; import static org.assertj.core.api.Java6Assertions.assertThat; public final class DefaultOnDataMismatchAdapterTest { @Test public void deserializeMismatch() throws IOException { Fruit fruit = buildMoshi(newFruitFactory()).adapter(Fruit.class).fromJson("\"mango\""); assertThat(fruit).isNull(); } @Test public void deserializeMatch() throws IOException { Fruit fruit = buildMoshi(newFruitFactory()).adapter(Fruit.class).fromJson("\"banana\""); assertThat(fruit).isEqualTo(BANANA); } @Test public void serialize() { String fruit = buildMoshi(newFruitFactory()).adapter(Fruit.class).toJson(APPLE); assertThat(fruit).isEqualTo("\"apple\""); } @Test public void factorySupportsType() throws Exception { Type parameterized = Types.newParameterizedType(List.class, String.class); List fallback = Collections.singletonList("test"); // Build a moshi instance using the adapter under test and one that will throw on each read. Moshi moshi = buildMoshi(DefaultOnDataMismatchAdapter.newFactory(parameterized, fallback)) .newBuilder() .add(new JsonAdapter.Factory() { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { final JsonAdapter next = moshi.nextAdapter(this, type, annotations); return new JsonAdapter() { @Override public Object fromJson(JsonReader reader) throws IOException { throw new JsonDataException("Fail for all types"); } @Override public void toJson(JsonWriter writer, Object value) throws IOException { next.toJson(writer, value); } }; } }) .build(); JsonAdapter> adapter = moshi.adapter(parameterized); List fromJson = adapter.fromJson("[]"); assertThat(fromJson) .isEqualTo(fallback) .containsExactly("test"); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("[\"test\"]"); } @Test public void toStringReflectsInner() { assertThat(buildMoshi(newFruitFactory()).adapter(Fruit.class).toString()) .isEqualTo("JsonAdapter(com.serjltt.moshi.adapters.DefaultOnDataMismatchAdapterTest$Fruit)" + ".nullSafe().defaultOnDatMisMatch(null)"); } private JsonAdapter.Factory newFruitFactory() { return DefaultOnDataMismatchAdapter.newFactory(Fruit.class, null); } private Moshi buildMoshi(JsonAdapter.Factory factory) { return new Moshi.Builder() .add(factory) .build(); } enum Fruit { @Json(name = "banana")BANANA, @Json(name = "apple")APPLE } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/DeserializeOnlyJsonAdapterTest.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public final class DeserializeOnlyJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(DeserializeOnly.ADAPTER_FACTORY) .add(new Custom.CustomAdapter()) // We need to check that other annotations are not lost. .build(); @Test public void deserializeOnly() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\"data\": \"test\"}"); assertThat(fromJson.data).isEqualTo("test"); assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); } @Test public void factoryMaintainsOtherAnnotations() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); Data2 fromJson = adapter.fromJson("{\"data\": \"test\"}"); assertThat(fromJson.data).isEqualTo("testCustom"); assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(String.class, DeserializeOnly.class); assertThat(adapter.toString()).isEqualTo("JsonAdapter(String).nullSafe().deserializeOnly()"); } private static class Data1 { @DeserializeOnly String data; } private static class Data2 { @DeserializeOnly @Custom String data; } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/ElementAtJsonAdapterTest.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.Json; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Set; import org.junit.Ignore; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; public final class ElementAtJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(ElementAt.ADAPTER_FACTORY) .add(new Custom.CustomAdapter()) // We need to check that other annotations are not lost. .build(); @Test public void elementAt() throws Exception { JsonAdapter adapter = moshi.adapter(Data.class); Data fromJson = adapter.fromJson("{\n" + " \"obj\": [\n" + " \"one\",\n" + " \"two\"\n" + " ]\n" + "}"); assertThat(fromJson.str).isEqualTo("two"); String toJson = adapter.toJson(fromJson); // The excluded data is lost during parsing // Adapter under test assumes that the consumer doesn't need that data assertThat(toJson).isEqualTo("{\"obj\":[\"two\"]}"); } @Test public void fromJsonOnEmptyArrayReturnsNull() throws Exception { assertNullReturn("{\n" + " \"obj\": []\n" + "}"); } @Test public void fromJsonOnArrayOfSizeOneReturnsNull() throws Exception { assertNullReturn("{\n" + " \"obj\": [\"test\"]\n" + "}"); } @Test public void fromJsonOnNullArrayReturnsNull() throws Exception { assertNullReturn("{\n" + " \"obj\": null\n" + "}"); } @Test public void fromJsonExpectsAnArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data.class); try { adapter.fromJson("{\n" + " \"obj\": \"this_will_throw\"\n" + "}"); fail(); } catch (JsonDataException e) { // Moshi's Collection adapter will throw assertThat(e).hasMessage("Expected BEGIN_ARRAY but was STRING at path $.obj"); } } // Currently there is no way to create an adapter @Test @Ignore public void factoryMaintainsOtherAnnotations() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); Data2 fromJson = adapter.fromJson("{\n" + " \"str\": [\n" + " \"test\"\n" + " ]\n" + "}"); assertThat(fromJson.str).isEqualTo("testCustom"); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"str\":[\"test\"]}"); } // This one is redundant, but keeps JaCoCo quiet @Test public void factoryExpectsOnlyOneAnnotation() { // A list of fake annotations. Set annotations = new LinkedHashSet() { { add(new Annotation() { @Override public Class annotationType() { return Test.class; } }); add(new Annotation() { @Override public Class annotationType() { return Custom.class; } }); } }; assertThat(ElementAt.ADAPTER_FACTORY.create(String.class, annotations, moshi)).isNull(); // Emulate existing annotation (should also return null). annotations.add(new ElementAt() { @Override public Class annotationType() { return ElementAt.class; } @Override public int index() { return 0; } }); assertThat(ElementAt.ADAPTER_FACTORY.create(String.class, annotations, moshi)).isNull(); } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(String.class, Collections.singleton(new ElementAt() { @Override public Class annotationType() { return ElementAt.class; } @Override public int index() { return 2; } })); assertThat(adapter.toString()) .isEqualTo("JsonAdapter(String).nullSafe().collection().nullSafe().elementAt(2)"); } @Test public void elementAtDelegated() throws Exception { JsonAdapter adapter = moshi.adapter(Data3.class); Data3 fromJson = adapter.fromJson("{\n" + " \"obj\": [\n" + " \"one\",\n" + " \"two\"\n" + " ]\n" + "}"); assertThat(fromJson.str).isEqualTo("two"); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"obj\":[\"two\"]}"); } @JsonQualifier @ElementAt(index = 1) @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) @interface AlwaysElementAtIndexOne { } private void assertNullReturn(String string) throws IOException { JsonAdapter adapter = moshi.adapter(Data.class); Data fromJson = adapter.fromJson(string); assertThat(fromJson.str).isNull(); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"obj\":[null]}"); } private static class Data { @ElementAt(index = 1) @Json(name = "obj") String str; } private static class Data2 { @ElementAt(index = 0) @Custom String str; } private static class Data3 { @AlwaysElementAtIndexOne @Json(name = "obj") String str; } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/FallbackEnumJsonAdapterTest.java ================================================ /* * Copyright 2016 Serj Lotutovici * Copyright (C) 2014 Square, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.Json; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.Moshi; import java.lang.annotation.Annotation; import java.util.Collections; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; public final class FallbackEnumJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(FallbackEnum.ADAPTER_FACTORY) .build(); @Test public void asRegularEnumAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(Roshambo.class).lenient(); assertThat(adapter.fromJson("\"ROCK\"")).isEqualTo(Roshambo.ROCK); assertThat(adapter.toJson(Roshambo.PAPER)).isEqualTo("\"PAPER\""); // Check annotated value assertThat(adapter.fromJson("\"scr\"")).isEqualTo(Roshambo.SCISSORS); assertThat(adapter.toJson(Roshambo.SCISSORS)).isEqualTo("\"scr\""); } @Test public void fallbackEnum() throws Exception { JsonAdapter adapter = moshi.adapter(Roshambo.class).lenient(); assertThat(adapter.fromJson("\"SPOCK\"")).isEqualTo(Roshambo.UNKNOWN); } @Test public void nullEnum() throws Exception { JsonAdapter adapter = moshi.adapter(Roshambo.class).lenient(); assertThat(adapter.fromJson("null")).isNull(); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void throwsOnInvalidFallback() throws Exception { try { moshi.adapter(Value.class); fail(); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessage( "No enum constant com.serjltt.moshi.adapters.FallbackEnumJsonAdapterTest.Value.UNK"); } } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(Roshambo.class); assertThat(adapter.toString()).isEqualTo( "JsonAdapter(com.serjltt.moshi.adapters.FallbackEnumJsonAdapterTest$Roshambo)" + ".fallbackEnum(UNKNOWN).nullSafe()"); } @Test public void ignoresUnannotatedEnums() throws Exception { JsonAdapter adapter = moshi.adapter(Regular.class).lenient(); assertThat(adapter.fromJson("\"ONE\"")).isEqualTo(Regular.ONE); try { adapter.fromJson("\"TWO\""); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessage( "Expected one of [ONE] but was TWO at path $"); } } @Test public void factoryIgnoresUnsupportedTypes() throws Exception { JsonAdapter adapter1 = FallbackEnum.ADAPTER_FACTORY .create(String.class, Collections.emptySet(), moshi); assertThat(adapter1).isNull(); JsonAdapter adapter2 = FallbackEnum.ADAPTER_FACTORY .create(Roshambo.class, Collections.singleton(Wrapped.Factory.create("")), moshi); assertThat(adapter2).isNull(); } @FallbackEnum(name = "UNKNOWN") enum Roshambo { ROCK, PAPER, @Json(name = "scr") SCISSORS, UNKNOWN } @FallbackEnum(name = "UNK") enum Value { @SuppressWarnings("unused") UNKNOWN } enum Regular { ONE } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/FallbackOnNullJsonAdapterTest.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.FromJson; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.ToJson; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Set; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public final class FallbackOnNullJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(FallbackOnNull.ADAPTER_FACTORY) .add(new Multiply.MultiplyAdapter()) .build(); @Test public void booleanFallbacks() throws Exception { assertForClass(WrapsBool.class, false, true, "{\"first\":false,\"second\":true}"); } private static class WrapsBool implements Wrapper { @FallbackOnNull boolean first; @FallbackOnNull(fallbackBoolean = true) boolean second; @Override public Boolean first() { return first; } @Override public Boolean second() { return second; } } @Test public void byteFallbacks() throws Exception { assertForClass(WrapsByte.class, Byte.MIN_VALUE, (byte) 42, "{\"first\":128,\"second\":42}"); } private static class WrapsByte implements Wrapper { @FallbackOnNull byte first; @FallbackOnNull(fallbackByte = 42) byte second; @Override public Byte first() { return first; } @Override public Byte second() { return second; } } @Test public void charFallbacks() throws Exception { assertForClass(WrapsChar.class, '\u0000', 'a', "{\"first\":\"\\u0000\",\"second\":\"a\"}"); } private static class WrapsChar implements Wrapper { @FallbackOnNull char first; @FallbackOnNull(fallbackChar = 'a') char second; @Override public Character first() { return first; } @Override public Character second() { return second; } } @Test public void doubleFallbacks() throws Exception { assertForClass(WrapsDouble.class, Double.MIN_VALUE, 12.0, "{\"first\":4.9E-324,\"second\":12.0}"); } private static class WrapsDouble implements Wrapper { @FallbackOnNull double first; @FallbackOnNull(fallbackDouble = 12.0) double second; @Override public Double first() { return first; } @Override public Double second() { return second; } } @Test public void floatFallbacks() throws Exception { assertForClass(WrapsFloat.class, Float.MIN_VALUE, 16.0f, "{\"first\":1.4E-45,\"second\":16.0}"); } private static class WrapsFloat implements Wrapper { @FallbackOnNull float first; @FallbackOnNull(fallbackFloat = 16.0f) float second; @Override public Float first() { return first; } @Override public Float second() { return second; } } @Test public void intFallbacks() throws Exception { assertForClass(WrapsInt.class, Integer.MIN_VALUE, -1, "{\"first\":-2147483648,\"second\":-1}"); } @Test public void intFallbacksNoLocaleInfluence() throws Exception { Locale defaultLocale = Locale.getDefault(); Locale.setDefault(new Locale("tr", "TR")); assertForClass(WrapsInt.class, Integer.MIN_VALUE, -1, "{\"first\":-2147483648,\"second\":-1}"); Locale.setDefault(defaultLocale); } private static class WrapsInt implements Wrapper { @FallbackOnNull int first; @FallbackOnNull(fallbackInt = -1) int second; @Override public Integer first() { return first; } @Override public Integer second() { return second; } } @Test public void longFallbacks() throws Exception { assertForClass(WrapsLong.class, Long.MIN_VALUE, -113L, "{\"first\":-9223372036854775808,\"second\":-113}"); } private static class WrapsLong implements Wrapper { @FallbackOnNull long first; @FallbackOnNull(fallbackLong = -113) long second; @Override public Long first() { return first; } @Override public Long second() { return second; } } @Test public void shortFallbacks() throws Exception { assertForClass(WrapsShort.class, Short.MIN_VALUE, (short) 121, "{\"first\":-32768,\"second\":121}"); } private static class WrapsShort implements Wrapper { @FallbackOnNull short first; @FallbackOnNull(fallbackShort = 121) short second; @Override public Short first() { return first; } @Override public Short second() { return second; } } @Test public void factoryMaintainsOtherAnnotations() throws Exception { JsonAdapter adapter = moshi.adapter(AnotherInt.class); AnotherInt fromJson = adapter.fromJson("{\n" + " \"willFallback\": null,\n" + " \"willMultiply\": 3\n" + "}"); assertThat(fromJson.willFallback).isEqualTo(2); assertThat(fromJson.willMultiply).isEqualTo(6); String toJson = adapter.toJson(fromJson); // Both values should be serialized by the Multiply json adapter. assertThat(toJson).isEqualTo("{\"willFallback\":1,\"willMultiply\":3}"); } private static class AnotherInt { @FallbackOnNull(fallbackInt = 2) @Multiply int willFallback; @FallbackOnNull(fallbackInt = 2) @Multiply int willMultiply; } @Test public void factoryIgnoresNonPrimitiveTypes() { List> classes = new ArrayList>() { { add(Boolean.class); add(Byte.class); add(Character.class); add(Double.class); add(Float.class); add(Integer.class); add(Long.class); add(Short.class); add(String.class); add(Object.class); } }; for (Class cls : classes) { assertThat(FallbackOnNull.ADAPTER_FACTORY.create(cls, ANNOTATIONS, moshi)).isNull(); } } @Test public void fallbackOnNullIsDelegated() throws Exception { JsonAdapter adapter = moshi.adapter(AndAnotherInt.class); AndAnotherInt fromJson = adapter.fromJson("{\n" + " \"willFallback\": null\n" + "}"); assertThat(fromJson.willFallback).isEqualTo(2); } private static class AndAnotherInt { @AlwaysFallBackToTwoOnNull int willFallback; } @JsonQualifier @FallbackOnNull(fallbackInt = 2) @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) @interface AlwaysFallBackToTwoOnNull { } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(int.class, ANNOTATIONS); assertThat(adapter.toString()) .isEqualTo("JsonAdapter(Integer).fallbackOnNull(fallbackInt=-1)"); } private static final Set ANNOTATIONS = Collections.singleton( new FallbackOnNull() { @Override public Class annotationType() { return FallbackOnNull.class; } @Override public boolean fallbackBoolean() { return false; } @Override public byte fallbackByte() { return 0; } @Override public char fallbackChar() { return 0; } @Override public double fallbackDouble() { return 0; } @Override public float fallbackFloat() { return 0; } @Override public int fallbackInt() { return -1; // Only this method will be taken into account } @Override public long fallbackLong() { return 0; } @Override public short fallbackShort() { return 0; } }); private , P> void assertForClass(Class cls, P first, P second, String asJson) throws IOException { JsonAdapter adapter = moshi.adapter(cls); T fromJson = adapter.fromJson("{\n" + " \"first\": null,\n" + " \"second\": null\n" + "}"); assertThat(fromJson.first()).isEqualTo(first); assertThat(fromJson.second()).isEqualTo(second); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo(asJson); } private interface Wrapper

{ P first(); P second(); } @JsonQualifier @Retention(RetentionPolicy.RUNTIME) private @interface Multiply { final class MultiplyAdapter { @Multiply @FromJson int fromJson(int val) { return val * 2; } @ToJson int toJson(@Multiply int val) { return val / 2; } } } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/FilterNullsJsonAdapterTest.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.junit.Test; import static java.util.Arrays.asList; import static org.assertj.core.api.Java6Assertions.assertThat; public final class FilterNullsJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(FilterNulls.ADAPTER_FACTORY) .add(new Custom.CustomAdapter()) // We need to check that other annotations are not lost. .build(); @Test public void noNullValues() throws Exception { JsonAdapter> adapter = moshi.adapter(Types.newParameterizedType(List.class, String.class), FilterNulls.class); List fromJson = adapter.fromJson("[\"apple\",\"banana\"]"); assertThat(fromJson).containsExactly("apple", "banana"); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("[\"apple\",\"banana\"]"); } @Test public void nullValues() throws Exception { JsonAdapter> adapter = moshi.adapter(Types.newParameterizedType(List.class, String.class), FilterNulls.class); List fromJson = adapter.fromJson("[\"apple\",\"banana\",null]"); assertThat(fromJson).containsExactly("apple", "banana"); String toJson = adapter.toJson(new ArrayList<>(asList("apple", "banana", null))); assertThat(toJson).isEqualTo("[\"apple\",\"banana\"]"); } @Test public void nullList() throws Exception { JsonAdapter> adapter = moshi.adapter(Types.newParameterizedType(List.class, String.class), FilterNulls.class); List fromJson = adapter.fromJson("null"); assertThat(fromJson).isNull(); String toJson = adapter.toJson(null); assertThat(toJson).isEqualTo("null"); } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(Types.newParameterizedType(Collection.class, String.class), FilterNulls.class); assertThat(adapter.toString()) .isEqualTo("JsonAdapter(String).nullSafe().collection().nullSafe().filterNulls()"); } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/FirstElementJsonAdapterTest.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.Json; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.Moshi; import java.io.IOException; import java.lang.annotation.Annotation; import java.util.LinkedHashSet; import java.util.Set; import org.junit.Ignore; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; public final class FirstElementJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(FirstElement.ADAPTER_FACTORY) .add(new Custom.CustomAdapter()) // We need to check that other annotations are not lost. .build(); @Test public void first() throws Exception { JsonAdapter adapter = moshi.adapter(Data.class); Data fromJson = adapter.fromJson("{\n" + " \"obj\": [\n" + " \"one\",\n" + " \"two\"\n" + " ]\n" + "}"); assertThat(fromJson.str).isEqualTo("one"); String toJson = adapter.toJson(fromJson); // The excluded data is lost during parsing // Adapter under test assumes that the consumer doesn't need that data assertThat(toJson).isEqualTo("{\"obj\":[\"one\"]}"); } @Test public void fromJsonOnEmptyArrayReturnsNull() throws Exception { assertNullReturn("{\n" + " \"obj\": []\n" + "}"); } @Test public void fromJsonOnNullArrayReturnsNull() throws Exception { assertNullReturn("{\n" + " \"obj\": null\n" + "}"); } @Test public void fromJsonExpectsAnArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data.class); try { adapter.fromJson("{\n" + " \"obj\": \"this_will_throw\"\n" + "}"); fail(); } catch (JsonDataException e) { // Moshi's Collection adapter will throw assertThat(e).hasMessage("Expected BEGIN_ARRAY but was STRING at path $.obj"); } } // Currently there is no way to create an adapter @Test @Ignore public void factoryMaintainsOtherAnnotations() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); Data2 fromJson = adapter.fromJson("{\n" + " \"str\": [\n" + " \"test\"\n" + " ]\n" + "}"); assertThat(fromJson.str).isEqualTo("testCustom"); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"str\":[\"test\"]}"); } // This one is redundant, but keeps JaCoCo quiet @Test public void factoryExpectsOnlyOneAnnotation() throws Exception { // A list of fake annotations. Set annotations = new LinkedHashSet() { { add(new Annotation() { @Override public Class annotationType() { return Test.class; } }); add(new Annotation() { @Override public Class annotationType() { return Custom.class; } }); } }; assertThat(FirstElement.ADAPTER_FACTORY.create(String.class, annotations, moshi)).isNull(); // Emulate existing annotation (should also return null). annotations.add(new Annotation() { @Override public Class annotationType() { return FirstElement.class; } }); assertThat(FirstElement.ADAPTER_FACTORY.create(String.class, annotations, moshi)).isNull(); } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(String.class, FirstElement.class); assertThat(adapter.toString()) .isEqualTo("JsonAdapter(String).nullSafe().collection().nullSafe().elementAt(0)"); } private void assertNullReturn(String string) throws IOException { JsonAdapter adapter = moshi.adapter(Data.class); Data fromJson = adapter.fromJson(string); assertThat(fromJson.str).isNull(); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"obj\":[null]}"); } private static class Data { @FirstElement @Json(name = "obj") String str; } private static class Data2 { @FirstElement @Custom String str; } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/LastElementJsonAdapterTest.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.Json; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.Moshi; import java.io.IOException; import java.lang.annotation.Annotation; import java.util.LinkedHashSet; import java.util.Set; import org.junit.Ignore; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; public final class LastElementJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(LastElement.ADAPTER_FACTORY) .add(new Custom.CustomAdapter()) // We need to check that other annotations are not lost. .build(); @Test public void last() throws Exception { JsonAdapter adapter = moshi.adapter(LastElementJsonAdapterTest.Data.class); LastElementJsonAdapterTest.Data fromJson = adapter.fromJson("{\n" + " \"obj\": [\n" + " \"one\",\n" + " \"two\"\n" + " ]\n" + "}"); assertThat(fromJson.str).isEqualTo("two"); String toJson = adapter.toJson(fromJson); // The excluded data is lost during parsing // Adapter under test assumes that the consumer doesn't need that data assertThat(toJson).isEqualTo("{\"obj\":[\"two\"]}"); } @Test public void fromJsonOnEmptyArrayReturnsNull() throws Exception { assertNullReturn("{\n" + " \"obj\": []\n" + "}"); } @Test public void fromJsonOnNullArrayReturnsNull() throws Exception { assertNullReturn("{\n" + " \"obj\": null\n" + "}"); } @Test public void fromJsonExpectsAnArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data.class); try { adapter.fromJson("{\n" + " \"obj\": \"this_will_throw\"\n" + "}"); fail(); } catch (JsonDataException e) { // Moshi's Collection adapter will throw assertThat(e).hasMessage("Expected BEGIN_ARRAY but was STRING at path $.obj"); } } // Currently there is no way to create an adapter @Test @Ignore public void factoryMaintainsOtherAnnotations() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); LastElementJsonAdapterTest.Data2 fromJson = adapter.fromJson("{\n" + " \"str\": [\n" + " \"test\"\n" + " ]\n" + "}"); assertThat(fromJson.str).isEqualTo("testCustom"); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"str\":[\"test\"]}"); } // This one is redundant, but keeps JaCoCo quiet @Test public void factoryExpectsOnlyOneAnnotation() throws Exception { // A list of fake annotations. Set annotations = new LinkedHashSet() { { add(new Annotation() { @Override public Class annotationType() { return Test.class; } }); add(new Annotation() { @Override public Class annotationType() { return Custom.class; } }); } }; assertThat(LastElement.ADAPTER_FACTORY.create(String.class, annotations, moshi)).isNull(); // Emulate existing annotation (should also return null). annotations.add(new Annotation() { @Override public Class annotationType() { return LastElement.class; } }); assertThat(LastElement.ADAPTER_FACTORY.create(String.class, annotations, moshi)).isNull(); } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(String.class, LastElement.class); assertThat(adapter.toString()) .isEqualTo("JsonAdapter(String).nullSafe().collection().nullSafe().lastElement()"); } private void assertNullReturn(String string) throws IOException { JsonAdapter adapter = moshi.adapter(Data.class); LastElementJsonAdapterTest.Data fromJson = adapter.fromJson(string); assertThat(fromJson.str).isNull(); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"obj\":[null]}"); } private static class Data { @LastElement @Json(name = "obj") String str; } private static class Data2 { @LastElement @Custom String str; } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/SerializeNullsJsonAdapterTest.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import okio.Buffer; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public final class SerializeNullsJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(SerializeNulls.ADAPTER_FACTORY) .add(new Custom.CustomAdapter()) // We need to check that other annotations are not lost. .build(); @Test public void serializesNulls() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + " \"data\": null\n" + "}"); assertThat(fromJson.data).isNull(); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"data\":null}"); } @Test public void factoryMaintainsOtherAnnotations() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); Data2 fromJson = adapter.fromJson("{\n" + " \"data\": \"val\"\n" + "}"); assertThat(fromJson.data).isEqualTo("valCustom"); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"data\":\"val\"}"); Data2 data = new Data2(); data.data = null; toJson = adapter.toJson(data); assertThat(toJson).isEqualTo("{\"data\":null}"); } @Test public void maintainsPreviousSerializationValue() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 data1 = new Data1(); JsonWriter writer1 = JsonWriter.of(new Buffer()); writer1.setSerializeNulls(true); adapter.toJson(writer1, data1); assertThat(writer1.getSerializeNulls()).isTrue(); JsonWriter writer2 = JsonWriter.of(new Buffer()); writer2.setSerializeNulls(false); adapter.toJson(writer2, data1); assertThat(writer2.getSerializeNulls()).isFalse(); } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(String.class, SerializeNulls.class); assertThat(adapter.toString()) .isEqualTo("JsonAdapter(String).nullSafe().serializeNulls()"); } private static class Data1 { @SerializeNulls String data; } private static class Data2 { @Custom @SerializeNulls String data; } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/SerializeOnlyJsonAdapterTest.java ================================================ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public final class SerializeOnlyJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(SerializeOnly.ADAPTER_FACTORY) .add(SerializeNulls.ADAPTER_FACTORY) .add(new Custom.CustomAdapter()) // We need to check that other annotations are not lost. .build(); @Test public void serializeOnly() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\"data\": \"test\"}"); assertThat(fromJson.data).isNull(); fromJson.data = "1234"; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"data\":\"1234\"}"); } @Test public void factoryMaintainsOtherAnnotations() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); Data2 fromJson = adapter.fromJson("{\"data\": \"test\"}"); assertThat(fromJson.data).isNull(); fromJson.data = "1234Custom"; String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"data\":\"1234\"}"); Data2 data = new Data2(); data.data = null; toJson = adapter.toJson(data); assertThat(toJson).isEqualTo("{\"data\":null}"); } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(String.class, SerializeOnly.class); assertThat(adapter.toString()).isEqualTo("JsonAdapter(String).nullSafe().serializeOnly()"); } private static class Data1 { @SerializeOnly String data; } private static class Data2 { @Custom @SerializeOnly @SerializeNulls String data; } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/SerializeOnlyNonEmptyJsonAdapterTest.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; public final class SerializeOnlyNonEmptyJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(SerializeOnlyNonEmpty.ADAPTER_FACTORY) .add(new Custom.CustomAdapter()) // We need to check that other annotations are not lost. .build(); @Test public void serializesOnlyNonEmptyCustomArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"customArray\": [{" + "\"data\":\"blub\"" + "}]\n" + "}"); assertThat(fromJson.customArray).isNotNull().hasSize(1); assertThat(fromJson.customArray[0].data).isEqualTo("blub"); fromJson.customArray = new CustomType[0]; assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.customArray = new CustomType[] { new CustomType("blub") }; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"customArray\":[{\"data\":\"blub\"}]}"); } @Test public void serializesOnlyNonEmptyByteArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"byteArray\": [1]\n" + "}"); assertThat(fromJson.byteArray).containsExactly((byte) 1); fromJson.byteArray = new byte[0]; assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.byteArray = new byte[] { 5 }; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"byteArray\":[5]}"); } @Test public void serializesOnlyNonEmptyCharArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"charArray\": [\"A\"]\n" + "}"); assertThat(fromJson.charArray).containsExactly((char) 65); fromJson.charArray = new char[0]; assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.charArray = new char[] { 65 }; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"charArray\":[\"A\"]}"); } @Test public void serializesOnlyNonEmptyShortArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"shortArray\": [1]\n" + "}"); assertThat(fromJson.shortArray).containsExactly((short) 1); fromJson.shortArray = new short[0]; assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.shortArray = new short[] { 5 }; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"shortArray\":[5]}"); } @Test public void serializesOnlyNonEmptyIntArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"intArray\": [1]\n" + "}"); assertThat(fromJson.intArray).containsExactly(1); fromJson.intArray = new int[0]; assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.intArray = new int[] { 5 }; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"intArray\":[5]}"); } @Test public void serializesOnlyNonEmptyLongArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"longArray\": [1]\n" + "}"); assertThat(fromJson.longArray).containsExactly(1L); fromJson.longArray = new long[0]; assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.longArray = new long[] { 5L }; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"longArray\":[5]}"); } @Test public void serializesOnlyNonEmptyFloatArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"floatArray\": [1.0]\n" + "}"); assertThat(fromJson.floatArray).containsExactly(1.f); fromJson.floatArray = new float[0]; assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.floatArray = new float[] { 5f }; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"floatArray\":[5.0]}"); } @Test public void serializesOnlyNonEmptyDoubleArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"doubleArray\": [1.0]\n" + "}"); assertThat(fromJson.doubleArray).containsExactly(1.f); fromJson.doubleArray = new double[0]; assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.doubleArray = new double[] { 5f }; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"doubleArray\":[5.0]}"); } @Test public void serializesOnlyNonEmptyBooleanArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"booleanArray\": [false]\n" + "}"); assertThat(fromJson.booleanArray).containsExactly(false); fromJson.booleanArray = new boolean[0]; assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.booleanArray = new boolean[] { false }; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"booleanArray\":[false]}"); } @Test public void serializesOnlyNonEmptyStringArray() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"stringArray\": [\"blub\"]\n" + "}"); assertThat(fromJson.stringArray).containsExactly("blub"); fromJson.stringArray = new String[0]; assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.stringArray = new String[] { "blub" }; assertThat(adapter.toJson(fromJson)).isEqualTo("{\"stringArray\":[\"blub\"]}"); } @Test public void serializesOnlyNonEmptyCollection() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"collection\": [\"blub\"]\n" + "}"); assertThat(fromJson.collection).containsExactly("blub"); fromJson.collection = new ArrayList<>(0); assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.collection.add("blub"); assertThat(adapter.toJson(fromJson)).isEqualTo("{\"collection\":[\"blub\"]}"); } @Test public void serializesOnlyNonEmptyMap() throws Exception { JsonAdapter adapter = moshi.adapter(Data1.class); Data1 fromJson = adapter.fromJson("{\n" + "\"map\": {" + "\"email\":\"blub\"\n" + "}\n" + "}"); assertThat(fromJson.map).containsEntry("email", "blub"); fromJson.map = new HashMap<>(); assertThat(adapter.toJson(fromJson)).isEqualTo("{}"); fromJson.map.put("email", "blub"); assertThat(adapter.toJson(fromJson)).isEqualTo("{\"map\":{\"email\":\"blub\"}}"); } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(String[].class, SerializeOnlyNonEmpty.class); assertThat(adapter.toString()) .isEqualTo("JsonAdapter(String).nullSafe().array().nullSafe().serializeOnlyNonEmpty()"); } static class Data1 { @SerializeOnlyNonEmpty CustomType[] customArray; @SerializeOnlyNonEmpty byte[] byteArray; @SerializeOnlyNonEmpty char[] charArray; @SerializeOnlyNonEmpty short[] shortArray; @SerializeOnlyNonEmpty int[] intArray; @SerializeOnlyNonEmpty long[] longArray; @SerializeOnlyNonEmpty float[] floatArray; @SerializeOnlyNonEmpty double[] doubleArray; @SerializeOnlyNonEmpty boolean[] booleanArray; @SerializeOnlyNonEmpty String[] stringArray; @SerializeOnlyNonEmpty Collection collection; @SerializeOnlyNonEmpty Map map; } static class CustomType { final String data; CustomType(final String data) { this.data = data; } } } ================================================ FILE: src/unitTest/java/com/serjltt/moshi/adapters/WrappedJsonAdapterTest.java ================================================ /* * Copyright 2016 Serj Lotutovici * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.serjltt.moshi.adapters; import com.pushtorefresh.private_constructor_checker.PrivateConstructorChecker; import com.squareup.moshi.FromJson; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.JsonEncodingException; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.Moshi; import com.squareup.moshi.ToJson; import com.squareup.moshi.Types; import java.io.IOException; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.Collections; import java.util.List; import org.junit.Test; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.fail; public final class WrappedJsonAdapterTest { // Lazy adapters work only within the context of moshi. private final Moshi moshi = new Moshi.Builder() .add(Wrapped.ADAPTER_FACTORY) .add(new Custom.CustomAdapter()) // We need to check that other annotations are not lost. .add(new ThrowingAdapter()) // We need to check that exceptions are propagated correctly. .build(); @Test public void oneObject() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); Data2 fromJson = adapter.fromJson("{\n" + " \"data\": {\n" + " \"1\": {\n" + " \"2\": {\n" + " \"str\": \"test\",\n" + " \"val\": 42\n" + " }\n" + " }\n" + " }\n" + "}"); assertThat(fromJson).isNotNull(); assertThat(fromJson.data.str).isEqualTo("test"); assertThat(fromJson.data.val).isEqualTo(42); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"data\":{\"1\":{\"2\":{\"str\":\"test\",\"val\":42}}}}"); } @Test public void arrayOfObjects() throws Exception { JsonAdapter> adapter = moshi.adapter( Types.newParameterizedType(List.class, Data2.class)); List fromJson = adapter.fromJson("[\n" + " {\n" + " \"data\": {\n" + " \"1\": {\n" + " \"2\": {\n" + " \"str\": \"funny\",\n" + " \"val\": 42\n" + " }\n" + " }\n" + " }\n" + " },\n" + " {\n" + " \"data\": {\n" + " \"1\": {\n" + " \"2\": {\n" + " \"str\": \"prime\",\n" + " \"val\": 43\n" + " }\n" + " }\n" + " }\n" + " }\n" + "]"); assertThat(fromJson.get(0).data.str).isEqualTo("funny"); assertThat(fromJson.get(0).data.val).isEqualTo(42); assertThat(fromJson.get(1).data.str).isEqualTo("prime"); assertThat(fromJson.get(1).data.val).isEqualTo(43); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("[" + "{\"data\":{\"1\":{\"2\":{\"str\":\"funny\",\"val\":42}}}}," + "{\"data\":{\"1\":{\"2\":{\"str\":\"prime\",\"val\":43}}}}" + "]"); } @Test public void failOnNotFound() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); try { adapter.fromJson("{\n" + " \"data\": {\n" + " \"1\": {\n" + " \"2\": null\n" + " }\n" + " }\n" + "}"); fail(); } catch (JsonDataException ex) { assertThat(ex).hasMessage( "Wrapped Json expected at path: [1, 2]. Found null at $.data.1.2"); } } @Test public void failOnNotFound2() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); try { adapter.fromJson("{\n" + " \"data\": {\n" + " \"1\": null\n" + " }\n" + "}"); fail(); } catch (JsonDataException ex) { assertThat(ex).hasMessage( "Wrapped Json expected at path: [1, 2]. Found null at $.data.1"); } } @Test public void failOnNotFoundFalse() throws Exception { JsonAdapter adapter = moshi.adapter(String.class, Collections.singleton(Wrapped.Factory.create(false, "one"))); String fromJson = adapter.fromJson("{\"one\":null}"); assertThat(fromJson).isEqualTo(null); } @Test public void notNullSafe() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); try { adapter.fromJson("{\n" + " \"data\": null\n" + "}"); fail(); } catch (JsonDataException expected) { } Data2 data2 = new Data2(); String toJson = adapter.toJson(data2); assertThat(toJson).isEqualTo("{}"); toJson = adapter.serializeNulls().toJson(data2); assertThat(toJson).isEqualTo("{\"data\":{\"1\":{\"2\":null}}}"); } @Test public void fromJsonSkipsNonPathValues() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); Data2 fromJson = adapter.fromJson("{\n" + " \"data\": {\n" + " \"should_be_skipped\": null,\n" + " \"1\": {\n" + " \"2\": {\n" + " \"str\": \"works\",\n" + " \"val\": 11\n" + " }\n" + " }\n" + "\n" + " }\n" + "}"); assertThat(fromJson.data.str).isEqualTo("works"); assertThat(fromJson.data.val).isEqualTo(11); } @Test public void fromJsonRemainingPathValues() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); Data2 fromJson = adapter.fromJson("{\n" + " \"data\": {\n" + " \"1\": {\n" + " \"2\": {\n" + " \"str\": \"works\",\n" + " \"val\": 11\n" + " }\n" + " },\n" + " \"should_be_skipped1\": null,\n" + " \"should_be_skipped2\": null\n" + " }\n" + "}"); assertThat(fromJson.data.str).isEqualTo("works"); assertThat(fromJson.data.val).isEqualTo(11); } @Test public void fromJsonOnIncorrectPath() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); try { adapter.fromJson("{\n" + " \"data\": {\n" + " \"2\": {\n" + " \"1\": null\n" + " }\n" + " }\n" + "}"); fail(); } catch (JsonDataException e) { assertThat(e).hasMessage("Wrapped Json expected at path: [1, 2]. Actual: $.data"); } } @Test public void fromJsonDoesNotSwallowIOExceptions() throws Exception { JsonAdapter adapter = moshi.adapter(Data4.class); try { adapter.fromJson("{\n" + " \"th\": {\n" + " \"1\": \"this_will_throw\"\n" + " }\n" + "}"); fail(); } catch (IOException e) { assertThat(e).hasMessage("ThrowingAdapter.fromJson"); } } @Test public void fromJsonDoesNotSwallowJsonEncodingExceptions() throws Exception { JsonAdapter adapter = moshi.adapter(Data2.class); try { adapter.fromJson("{\n" + " \"data\": {\n" + " \"1\": {\n" + " \"2\": {\n" + " \"str\": \"valid\",\n" + " \"val\": NaN\n" + " }\n" + " }\n" + " }\n" + "}"); fail(); } catch (JsonEncodingException e) { assertThat(e).hasMessage( "Use JsonReader.setLenient(true) to accept malformed JSON at path $.data.1.2.val"); } } @Test public void fromJsonDoesNotSwallowJsonDataExceptions() throws Exception { JsonAdapter adapter = moshi.adapter(Data3.class); try { adapter.fromJson("{\n" + " \"str\": {\n" + " \"1\": false\n" + " }\n" + "}"); fail(); } catch (JsonDataException e) { assertThat(e).hasMessage("Expected a string but was BOOLEAN at path $.str.1"); } } @Test public void toJsonDoesNotSwallowExceptions() throws Exception { JsonAdapter adapter = moshi.adapter(Data4.class); Data4 data4 = new Data4(); data4.th = new Throws(); try { adapter.toJson(data4); fail(); } catch (Throwable e) { // Moshi wraps write exceptions in an AssertionError assertThat(e.getCause()).hasMessage("ThrowingAdapter.toJson"); } } @Test public void factoryMaintainsOtherAnnotations() throws Exception { JsonAdapter adapter = moshi.adapter(Data3.class); Data3 fromJson = adapter.fromJson("{\n" + " \"str\": {\n" + " \"1\": \"test\"\n" + " }\n" + "}"); assertThat(fromJson.str).isEqualTo("testCustom"); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"str\":{\"1\":\"test\"}}"); } @Test public void toStringReflectsInnerAdapter() throws Exception { JsonAdapter adapter = moshi.adapter(String.class, Collections.singleton(Wrapped.Factory.create("1", "2"))); assertThat(adapter.toString()) .isEqualTo("JsonAdapter(String).nullSafe().wrapped([1, 2]).failOnNotFound()"); JsonAdapter failingAdapter = moshi.adapter(String.class, Collections.singleton(Wrapped.Factory.create(false, "1", "2"))); assertThat(failingAdapter.toString()) .isEqualTo("JsonAdapter(String).nullSafe().wrapped([1, 2])"); } @Test public void wrappedFactoryRespectsEquals() throws Exception { Wrapped wrapped1 = Wrapped.Factory.create("one", "two", "three"); Wrapped wrapped2 = Wrapped.Factory.create("one", "two", "three"); Wrapped wrapped3 = Wrapped.Factory.create("one", "two", "four"); assertThat(wrapped1).isEqualTo(wrapped2); assertThat(wrapped1.hashCode()).isEqualTo(wrapped2.hashCode()); assertThat(wrapped1.toString()).isEqualTo(wrapped2.toString()); assertThat(wrapped1).isNotEqualTo(wrapped3); assertThat(wrapped1.hashCode()).isNotEqualTo(wrapped3.hashCode()); assertThat(wrapped1.toString()).isNotEqualTo(wrapped3.toString()); } @Test public void checkWrappedFactoryConstructorThrows() throws Exception { PrivateConstructorChecker .forClass(Wrapped.Factory.class) .expectedTypeOfException(AssertionError.class) .expectedExceptionMessage("No instances.") .check(); } @Test public void factoryFetchesWrappedFromDelegate() throws Exception { JsonAdapter adapter = moshi.adapter(Data5.class); Data5 fromJson = adapter.fromJson("{\n" + " \"str\": {\n" + " \"1\": \"test\"\n" + " }\n" + "}"); assertThat(fromJson.str).isEqualTo("test"); String toJson = adapter.toJson(fromJson); assertThat(toJson).isEqualTo("{\"str\":{\"1\":\"test\"}}"); } private static class Data1 { String str; int val; } private static class Data2 { @Wrapped(path = { "1", "2" }) Data1 data; } private static class Data3 { @Custom @Wrapped(path = "1") String str; } private static class Data4 { @Wrapped(path = "1") Throws th; } private static class Data5 { @WrappedDelegate String str; } private static class Throws { } @JsonQualifier @Wrapped(path = "1") @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER }) @interface WrappedDelegate { } /** String adapter, that will throw on read and write. */ private static final class ThrowingAdapter { @FromJson Throws fromJson(String str) throws IOException { throw new IOException("ThrowingAdapter.fromJson"); } @ToJson String toJson(Throws th) throws IOException { throw new IOException("ThrowingAdapter.toJson"); } } }