Repository: square/moshi Branch: master Commit: 2d8a3cc7112f Files: 183 Total size: 1.2 MB Directory structure: gitextract_wxuqnsxb/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ ├── renovate.json5 │ └── workflows/ │ ├── .java-version │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── build.gradle.kts ├── examples/ │ ├── build.gradle.kts │ └── src/ │ └── main/ │ └── java/ │ └── com/ │ └── squareup/ │ └── moshi/ │ └── recipes/ │ ├── ByteStrings.java │ ├── CardAdapter.java │ ├── CustomAdapterFactory.java │ ├── CustomAdapterWithDelegate.java │ ├── CustomFieldName.java │ ├── CustomQualifier.java │ ├── CustomTypeAdapter.java │ ├── DefaultOnDataMismatchAdapter.java │ ├── FallbackEnum.java │ ├── FromJsonWithoutStrings.java │ ├── IncludeNullsForAnnotatedTypes.java │ ├── IncludeNullsForOneType.java │ ├── JsonString.kt │ ├── MultipleFormats.java │ ├── ReadAndWriteRfc3339Dates.java │ ├── ReadJson.java │ ├── ReadJsonList.java │ ├── ReadJsonListKt.kt │ ├── RecoverFromTypeMismatch.java │ ├── Unwrap.java │ ├── WriteJson.java │ ├── models/ │ │ ├── BlackjackHand.java │ │ ├── Card.java │ │ ├── Player.java │ │ ├── Suit.java │ │ └── Tournament.java │ └── package-info.java ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── moshi/ │ ├── build.gradle.kts │ ├── gradle.properties │ ├── japicmp/ │ │ └── build.gradle.kts │ ├── records-tests/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── test/ │ │ └── java/ │ │ └── com/ │ │ └── squareup/ │ │ └── moshi/ │ │ └── records/ │ │ └── RecordsTest.java │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── squareup/ │ │ │ └── moshi/ │ │ │ ├── -JsonUtf8Reader.kt │ │ │ ├── -JsonUtf8Writer.kt │ │ │ ├── -JsonValueReader.kt │ │ │ ├── -JsonValueWriter.kt │ │ │ ├── -MoshiKotlinExtensions.kt │ │ │ ├── -MoshiKotlinTypesExtensions.kt │ │ │ ├── FromJson.kt │ │ │ ├── Json.kt │ │ │ ├── JsonAdapter.kt │ │ │ ├── JsonClass.kt │ │ │ ├── JsonDataException.kt │ │ │ ├── JsonEncodingException.kt │ │ │ ├── JsonQualifier.kt │ │ │ ├── JsonReader.kt │ │ │ ├── JsonWriter.kt │ │ │ ├── Moshi.kt │ │ │ ├── ToJson.kt │ │ │ ├── Types.kt │ │ │ ├── internal/ │ │ │ │ ├── AdapterMethodsFactory.kt │ │ │ │ ├── ArrayJsonAdapter.kt │ │ │ │ ├── ClassFactory.kt │ │ │ │ ├── ClassJsonAdapter.kt │ │ │ │ ├── CollectionJsonAdapter.kt │ │ │ │ ├── JsonScope.kt │ │ │ │ ├── JsonValueSource.kt │ │ │ │ ├── KotlinReflectTypes.kt │ │ │ │ ├── LinkedHashTreeMap.kt │ │ │ │ ├── MapJsonAdapter.kt │ │ │ │ ├── NonNullJsonAdapter.kt │ │ │ │ ├── NullSafeJsonAdapter.kt │ │ │ │ ├── RecordJsonAdapter.kt │ │ │ │ ├── StandardJsonAdapters.kt │ │ │ │ └── Util.kt │ │ │ └── package-info.java │ │ ├── java16/ │ │ │ └── com/ │ │ │ └── squareup/ │ │ │ └── moshi/ │ │ │ └── internal/ │ │ │ └── RecordJsonAdapter.kt │ │ └── resources/ │ │ └── META-INF/ │ │ └── proguard/ │ │ └── moshi.pro │ └── test/ │ └── java/ │ ├── android/ │ │ └── util/ │ │ └── Pair.java │ └── com/ │ └── squareup/ │ └── moshi/ │ ├── AdapterMethodsTest.java │ ├── CircularAdaptersTest.java │ ├── DeferredAdapterTest.java │ ├── FlattenTest.java │ ├── JsonAdapterTest.java │ ├── JsonCodecFactory.java │ ├── JsonQualifiersTest.java │ ├── JsonReaderPathTest.java │ ├── JsonReaderTest.java │ ├── JsonUtf8ReaderTest.java │ ├── JsonUtf8WriterTest.java │ ├── JsonValueReaderTest.java │ ├── JsonValueWriterTest.java │ ├── JsonWriterPathTest.java │ ├── JsonWriterTest.java │ ├── KotlinExtensionsTest.kt │ ├── MoshiTest.java │ ├── MoshiTesting.kt │ ├── ObjectAdapterTest.java │ ├── PromoteNameToValueTest.java │ ├── RecursiveTypesResolveTest.java │ ├── TestUtil.java │ ├── TypesTest.java │ └── internal/ │ ├── ClassJsonAdapterTest.java │ ├── JsonValueSourceTest.java │ ├── KotlinReflectTypesTest.kt │ ├── LinkedHashTreeMapTest.java │ └── MapJsonAdapterTest.java ├── moshi-adapters/ │ ├── README.md │ ├── build.gradle.kts │ ├── japicmp/ │ │ └── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── squareup/ │ │ └── moshi/ │ │ ├── Rfc3339DateJsonAdapter.kt │ │ └── adapters/ │ │ ├── EnumJsonAdapter.kt │ │ ├── Iso8601Utils.kt │ │ ├── PolymorphicJsonAdapterFactory.kt │ │ └── Rfc3339DateJsonAdapter.kt │ └── test/ │ └── java/ │ └── com/ │ └── squareup/ │ └── moshi/ │ └── adapters/ │ ├── EnumJsonAdapterTest.java │ ├── PolymorphicJsonAdapterFactoryTest.java │ └── Rfc3339DateJsonAdapterTest.java ├── moshi-kotlin/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── squareup/ │ │ │ └── moshi/ │ │ │ ├── KotlinJsonAdapterFactory.kt │ │ │ └── kotlin/ │ │ │ └── reflect/ │ │ │ ├── IndexedParameterMap.kt │ │ │ ├── Invokable.kt │ │ │ ├── JvmDescriptors.kt │ │ │ ├── JvmSignatureSearcher.kt │ │ │ ├── KmExecutable.kt │ │ │ ├── KotlinJsonAdapterFactory.kt │ │ │ └── KtTypes.kt │ │ └── resources/ │ │ └── META-INF/ │ │ └── com.android.tools/ │ │ ├── proguard/ │ │ │ └── moshi-metadata-reflect.pro │ │ ├── r8-from-1.6.0/ │ │ │ └── moshi-metadata-reflect.pro │ │ └── r8-upto-1.6.0/ │ │ └── moshi-metadata-reflect.pro │ └── test/ │ └── java/ │ └── com/ │ └── squareup/ │ └── moshi/ │ └── kotlin/ │ └── reflect/ │ └── KotlinJsonAdapterTest.kt ├── moshi-kotlin-codegen/ │ ├── build.gradle.kts │ └── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── squareup/ │ │ └── moshi/ │ │ └── kotlin/ │ │ └── codegen/ │ │ ├── api/ │ │ │ ├── AdapterGenerator.kt │ │ │ ├── DelegateKey.kt │ │ │ ├── InternalMoshiCodegenApi.kt │ │ │ ├── Options.kt │ │ │ ├── ProguardRules.kt │ │ │ ├── PropertyGenerator.kt │ │ │ ├── TargetConstructor.kt │ │ │ ├── TargetParameter.kt │ │ │ ├── TargetProperty.kt │ │ │ ├── TargetType.kt │ │ │ ├── TypeRenderer.kt │ │ │ ├── kotlintypes.kt │ │ │ └── typeAliasUnwrapping.kt │ │ └── ksp/ │ │ ├── AppliedType.kt │ │ ├── JsonClassSymbolProcessorProvider.kt │ │ ├── KspUtil.kt │ │ ├── MoshiApiUtil.kt │ │ ├── TargetTypes.kt │ │ └── shadedUtil.kt │ └── test/ │ └── java/ │ └── com/ │ └── squareup/ │ └── moshi/ │ └── kotlin/ │ └── codegen/ │ ├── JavaSuperclass.java │ └── ksp/ │ └── JsonClassSymbolProcessorTest.kt ├── moshi-kotlin-tests/ │ ├── build.gradle.kts │ ├── codegen-only/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── test/ │ │ └── kotlin/ │ │ └── com/ │ │ └── squareup/ │ │ └── moshi/ │ │ └── kotlin/ │ │ └── codegen/ │ │ ├── CompileOnlyTests.kt │ │ ├── ComplexGenericsInheritanceTest.kt │ │ ├── DefaultConstructorTest.kt │ │ ├── GeneratedAdaptersTest.kt │ │ ├── GeneratedAdaptersTest_CustomGeneratedClassJsonAdapter.kt │ │ ├── LooksLikeAClass/ │ │ │ └── ClassInPackageThatLooksLikeAClass.kt │ │ ├── MixingReflectAndCodeGen.kt │ │ ├── MoshiKspTest.kt │ │ ├── MultipleMasksTest.kt │ │ └── annotation/ │ │ └── UppercaseInAnnotationPackage.kt │ ├── extra-moshi-test-module/ │ │ ├── build.gradle.kts │ │ └── src/ │ │ └── main/ │ │ └── kotlin/ │ │ └── com/ │ │ └── squareup/ │ │ └── moshi/ │ │ └── kotlin/ │ │ └── codegen/ │ │ └── test/ │ │ └── extra/ │ │ └── AbstractClassInModuleA.kt │ └── src/ │ └── test/ │ └── kotlin/ │ └── com/ │ └── squareup/ │ └── moshi/ │ └── kotlin/ │ ├── DualKotlinTest.kt │ └── reflect/ │ └── KotlinJsonAdapterTest.kt ├── releasing.md └── settings.gradle.kts ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_size = 2 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true max_line_length=100 [*.{kt, kts}] kotlin_imports_layout = ascii ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf *.bat text eol=crlf *.jar binary ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: A reproducible problem title: '' labels: bug assignees: '' --- Good bug reports include a failing test! Writing a test helps you to isolate and describe the problem, and it helps us to fix it fast. Bug reports without a failing test or reproduction steps are likely to be closed. Here’s an example test to get you started. https://gist.github.com/ZacSweers/0ef2c5f0e3f61f82063dd8c67d75879f ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Question? url: https://stackoverflow.com/questions/tagged/moshi about: Please ask usage questions on Stack Overflow with the `moshi` tag. ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea title: '' labels: enhancement assignees: '' --- Start by telling us what problem you’re trying to solve. Often a solution already exists! Don’t send pull requests to implement new features without first getting our support. Sometimes we leave features out on purpose to keep the project small. ================================================ FILE: .github/renovate.json5 ================================================ { $schema: 'https://docs.renovatebot.com/renovate-schema.json', extends: [ 'config:recommended', ], ignorePresets: [ // Ensure we get the latest version and are not pinned to old versions. 'workarounds:javaLTSVersions', ], customManagers: [ // Update .java-version file with the latest JDK version. { customType: 'regex', fileMatch: [ '\\.java-version$', ], matchStrings: [ '(?.*)\\n', ], datasourceTemplate: 'java-version', depNameTemplate: 'java', // Only write the major version. extractVersionTemplate: '^(?\\d+)', }, ] } ================================================ FILE: .github/workflows/.java-version ================================================ 25 ================================================ FILE: .github/workflows/build.yml ================================================ name: CI on: [push, pull_request] jobs: build: name: 'Test Mode ${{ matrix.kotlin-test-mode }}' runs-on: ubuntu-latest strategy: fail-fast: false matrix: kotlin-test-mode: [ 'REFLECT', 'KSP' ] steps: - name: Checkout uses: actions/checkout@v6 - uses: actions/setup-java@v5 with: distribution: 'zulu' java-version-file: .github/workflows/.java-version - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Test run: ./gradlew build check --stacktrace -PkotlinTestMode=${{ matrix.kotlin-test-mode }} - name: Publish (default branch only) if: github.repository == 'square/moshi' && github.ref == 'refs/heads/master' && matrix.kotlin-test-mode == 'reflect' run: ./gradlew publish env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SECRET_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SECRET_PASSPHRASE }} ================================================ FILE: .github/workflows/release.yml ================================================ name: release on: push: tags: - '**' jobs: release: runs-on: macos-latest steps: - uses: actions/checkout@v6 - uses: actions/setup-java@v5 with: distribution: 'zulu' java-version-file: .github/workflows/.java-version - uses: gradle/actions/setup-gradle@v5 - run: ./gradlew publish dokkaGenerate -PmavenCentralDeploymentValidation=PUBLISHED env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_SECRET_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_SECRET_PASSPHRASE }} - name: Extract release notes id: release_notes uses: ffurrer2/extract-release-notes@v3 - name: Create release uses: softprops/action-gh-release@v2 with: body: ${{ steps.release_notes.outputs.release_notes }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Deploy docs to website uses: JamesIves/github-pages-deploy-action@releases/v3 with: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} BRANCH: site FOLDER: docs/2.x/ TARGET_FOLDER: docs/2.x/ CLEAN: true ================================================ FILE: .gitignore ================================================ .classpath .project .settings eclipsebin bin gen build out lib target pom.xml.* release.properties dependency-reduced-pom.xml .idea *.iml *.ipr *.iws classes obj .DS_Store .gradle # Temporary until generating a docsite docs/ ================================================ FILE: CHANGELOG.md ================================================ # Change Log ## Unreleased * None yet. ## [2.0.0-alpha.1] - 2026-01-28 * Refuse `j$.*` types from Android library desugaring as platform types. * In-development snapshots are now published to the Central Portal Snapshots repository at https://central.sonatype.com/repository/maven-snapshots/. * Fully supports encoding/decoding of value classes in both moshi-kotlin and code gen. * Note that Moshi does not propagate inlining to JSON by default. For example: `@JvmInline value class Color(val raw: Int)` is serialized to `{"raw": 12345}`. * New `@JsonClass.inline` property to allow inlining single-property JSON classes during encoding/decoding. * This is particularly useful for value classes. * For example, a class `@JvmInline value class UserId(val id: Int)` with `inline = true` will serialize as just `123` rather than `{"id": 123}`. * `PolymorphicJsonAdapterFactory` now invokes the fallback adapter when the label key is missing entirely from the JSON, not just when the label value is unrecognized. ### Upgrading to Moshi 2.x In 2.x, we upgraded Moshi’s source code from .java to .kt. This update is binary-compatible with Moshi 1.x, and so you can safely upgrade without recompiling your libraries. The update is not source-compatible for Kotlin users. #### JsonAdapter.Factory MutableSet Kotlin defines both a `Set` and a `MutableSet` type, but Java has only `Set`. Implementations of Moshi’s `JsonAdapter.Factory` interface may declare a function that accepts a `MutableSet`: ```kotlin override fun create(type: Type, annotations: MutableSet, moshi: Moshi): JsonAdapter<*>? { ... } ``` Moshi 2.x requires the `annotations` argument be a `Set`: ```kotlin override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { ... } ``` We used this shell script to migrate our code: ```shell find . \ -name "*.kt" \ -exec sed -i "" 's/annotations: MutableSet/annotations: Set/g' {} \; ``` #### Propagated Nullability in `JsonAdapter` Previously, due to limitations of the Java type system, Moshi would always treat the type as nullable. Kotlin's type system supports nullability natively, and Moshi now propagates nullability information from the generic type of `JsonAdapter`. - Calling `nullSafe()` on any `JsonAdapter` returns a `JsonAdapter` where `T` is the original type. - Calling `nonNull()` on any `JsonAdapter` returns a `JsonAdapter` where `T` is the original type. - Calling the reified `Moshi.adapter()` function overloads returns a `JsonAdapter` where `T` matches and respects the nullability of the reified type. - Calling the `Moshi.adapter(KType)` function overloads return a `JsonAdapter` that respects the nullability of the `KType` but trusts that the caller specifies the correct nullability on the generic type argument. - Calling any of the other `Moshi.adapter()` functions (that accept either `Type`, `Class`, or `KClass` types) always return nullable `JsonAdapter`s. This is often going to be a source-breaking change for Kotlin users, but ABI-compatible. All of Moshi's internals and code gen continue to function the same way. #### Explicit Nullability Moshi has functions like `JsonAdapter.fromJson(String)` that have always been _documented_ to require non-null parameters. With Moshi 2.x that requirement is enforced by the compiler. You may need to manually make parameters non-null (such as with `!!`) when you upgrade to Moshi 2.x. #### Properties We promoted `JsonWriter.setIndent()` to be a property. If you aren't already, you must use property syntax: ```diff val jsonWriter = JsonWriter.of(buffer) - jsonWriter.setIndent(" ") + jsonWriter.indent = " " ``` #### moshi-kotlin-codegen The KSP code generator now supports (and requires) KSP2. #### moshi-kotlin This release switches `KotlinJsonAdapterFactory` to use `kotlin-metadata` instead of `kotlin-reflect`. This is a lightweight alternative to kotlin-reflect and satisfies moshi-kotlin's requirements. It is also ~20% more performant at runtime. * This is not a source or ABI breaking change but if you were relying on the transitive `kotlin-reflect` dependency you will need to add it explicitly. * No longer encodes properties/fields from supertypes that are platform types. ## [1.15.2] - 2024-12-05 * Do not generate conditional shrinker rules (i.e., `-if`) when the rule is already predicated on the presence of the target type (as `-keepnames` and `-keepclassmembers` are). This will improve shrinker performance for projects with hundreds of model types, as conditional rules are more expensive to evaluate. ## [1.15.1] - 2024-01-30 * Upgrade to Okio `3.7.0` ## [1.15.0] - 2023-05-12 * Upgrade to Kotlin `1.8.21`. * Upgrade to KSP `1.8.21-1.0.11`. * Upgrade to kotlinx-metadata `0.6.0`. * Upgrade to KotlinPoet `1.13.2`. * Support Kotlin language version `1.9.0` in KAPT. * Include name of the type being processed when creating the adapterGenerator fails in KSP. * Suppress `UNUSED_PARAMETER` in generated code. * Deprecate KAPT code gen. Please migrate to KSP, we will remove KAPT support in a future release. This release will also print an annoying noisy warning in KAPT processing if you use it. ## [1.14.0] - 2022-09-06 * Upgrade: [Kotlin 1.7.0][kotlin_1_7_0]. * Upgrade [KotlinPoet 1.12.0][kotlinpoet_1_12_0]. * Moshi no longer shades kotlinpoet-ksp APIs, meaning they can be updated independently of Moshi updates. * Upgrade: [KSP 1.7.0-1.0.6][ksp_1_7_0_1_0_6]. * Upgrade: [kotlinx-metadata 0.5.0][kotlinx_metadata_0_5_0], allowing reading of kotlin 1.8 APIs too. ## [1.13.0] - 2021-12-08 * New: Support for [Kotlin Symbol Processing (KSP)][ksp]. KSP is an alternative to annotation processing code gen. It's builds faster and better supports Kotlin language features. To use KSP in your application you must enable the KSP Gradle plugin and add a KSP dependency on Moshi codegen. ``` plugins { id("com.google.devtools.ksp").version("1.6.0-1.0.1") } dependencies { ksp("com.squareup.moshi:moshi-kotlin-codegen:1.13.0") } ``` If you're switching from annotation processing (kapt) to KSP, you should remove the kapt plugin and the kapt Moshi dependency. * New: `@Json(ignore = true)` is a Moshi-specific way to exclude properties from JSON encoding and decoding. * New: Support Java 16 records. (Moshi still requires Java 8 only; we're shipping a [multi-release jar][mrjar] to better support modern JVMs.) * New: Option to disable generating R8/ProGuard files. These files prevent code shrinkers from removing files that support JSON processing. If you're not using ProGuard or R8, you can skip this step. ``` ksp { arg("moshi.generateProguardRules", "false") } ``` * Upgrade: [Kotlin 1.6.0][kotlin_1_6_0]. ## [1.12.0] - 2021-04-01 * New: Improve generated code performance when all properties are set. * Fix: Don't crash on a missing type element like `@SuppressLint`. * Fix: Update the JVM metadata library to avoid problems on Kotlin 1.5.0-M2. * Fix: Support generic arrays with defaults in generated adapters. * Fix: Don't generate code with simple name collisions. * Upgrade: [Okio 2.10.0][okio_2_10_0]. * Upgrade: [Kotlin 1.4.31][kotlin_1_4_31]. ## [1.11.0] - 2020-10-04 * New: Kotlin extension functions and properties. Use of these extensions is only possible from Kotlin, and requires the Kotlin stdlib dependency. This release does not have any Kotlin requirement and can be used Kotlin-free from Java. ```kotlin /** Extension alternative to [Types.nextAnnotations()]. */ fun Set.nextAnnotations(): Set? /** Extension alternative to [Types.getRawType()]. */ val Type.rawType: Class<*> /** Extension alternative to [Types.arrayOf()]. */ fun KClass<*>.asArrayType(): GenericArrayType /** Extension alternative to [Types.arrayOf()]. */ fun Type.asArrayType(): GenericArrayType ``` * New: Experimental Kotlin extensions. These depend on unreleased APIs and may break in a future release of Kotlin. If you are comfortable with this, add `@ExperimentalStdlibApi` at the callsite or add this argument to your Kotlin compiler: `"-Xopt-in=kotlin.ExperimentalStdlibApi"`. ```kotlin /** Returns the adapter for [T]. */ inline fun Moshi.adapter(): JsonAdapter /** Returns the adapter for [ktype]. */ fun Moshi.adapter(ktype: KType): JsonAdapter /** Adds an adapter for [T]. */ inline fun Moshi.Builder.addAdapter(adapter: JsonAdapter): Moshi.Builder /** Extension alternative to [Types.arrayOf()]. */ fun KType.asArrayType(): GenericArrayType /** Extension alternative to [Types.subtypeOf()]. */ inline fun subtypeOf(): WildcardType /** Extension alternative to [Types.supertypeOf()]. */ inline fun supertypeOf(): WildcardType ``` * New: `JsonReader.nextSource()`. This returns an Okio `BufferedSource` that streams the UTF-8 bytes of a JSON value. Use this to accept JSON values without decoding them, to delegate to another JSON processor, or for streaming access to very large embedded values. * New: `Moshi.Builder.addLast()`. Use this when installing widely-applicable adapter factories like `KotlinJsonAdapterFactory`. Adapters registered with `add()` are preferred (in the order they were added), followed by all adapters registered with `addLast()` (also in the order they were added). This precedence is retained when `Moshi.newBuilder()` is used. * New: `setTag()`, `tag()` methods on `JsonReader` and `JsonWriter`. Use these as a side-channel between adapters and their uses. For example, a tag may be used to track use of unexpected data in a custom adapter. * Fix: Don't crash with a `StackOverflowError` decoding backward-referencing type variables in Kotlin. This caused problems for parameterized types like `MyInterface>`. * Upgrade: [Okio 1.17.5][okio_1_7_5]. * Upgrade: [Kotlin 1.4.10][kotlin_1_4_10]. ## [1.10.0] - 2020-08-26 * New: Upgrade to Kotlin 1.4.0. * New: `JsonReader.promoteNameToValue()` makes it easier to build custom `Map` adapters. * New: `Options.strings()`. * New: `PolymorphicJsonAdapterFactory.withFallbackJsonAdapter()` makes it possible to handle unrecognized types when encoding and decoding. * New: Add `JsonWriter.jsonValue` API * New: Code gen now generates precise proguard rules on-the-fly. * New: Improve error when incorrectly trying to use a collection class like `ArrayList` instead of `List` * Fix: Prevent R8 from keeping all `@Metadata` annotations * Fix: Avoid VerifyErrors on Android 4.4 devices when using R8 * Fix: Fix resolution of types in superclass settable properties ## Version 1.9.3 _2020-06-11_ * Fix: Tweak a shrinker rule to mitigate an R8 bug which was causing classes unrelated to the Kotlin adpater code generation to be retained. * Fix: Ensure that the Kotlin adapter code generation does not line wrap in the middle of a string if your JSON keys contain spaces. * Fix: Strip type annotations before emitting type references like `Foo::class` in the Kotlin adapter code generation. * Fix: Separate the runtime check for Kotlin's `DefaultConstructorMarker` from the check for `Metadata`. A shrinker may have removed `Metadata` and we should still check for `DefaultConstructorMarker`. ## Version 1.9.2 _2019-11-17_ * Fix: Generate correct adapters for several special cases including reified inline types, public classes enclosed in internal classes, deprecated types with `-Werror`, primitives in type parameters, nullables in type parameters, and type aliases in type parameters. ## Version 1.9.1 _2019-10-30_ * Fix: "abstract function ... cannot have code" code gen crash when parsing Kotlin metadata. * Fix: Generate code to support constructors with more than 32 parameters. The 1.9.0 release had a regression where classes with 33+ parameters would crash upon decoding. * Fix: Generate code to support more constructor cases, such as classes with non-property parameters and classes with multiple constructors. * Fix: Generate code to handle type aliases in type parameters. ## Version 1.9.0 _2019-10-29_ * **This release requires kotlin-reflect or moshi-kotlin-codegen for all Kotlin classes.** Previously Moshi wouldn't differentiate between Kotlin classes and Java classes if Kotlin was not configured. This caused bad runtime behavior such as putting null into non-nullable fields! If you attempt to create an adapter for a Kotlin type, Moshi will throw an `IllegalArgumentException`. Fix this with either the reflection adapter: ```kotlin val moshi = Moshi.Builder() // ... add your own JsonAdapters and factories ... .add(KotlinJsonAdapterFactory()) .build() ``` Or the codegen annotation processor: ```kotlin @JsonClass(generateAdapter = true) data class BlackjackHand( val hidden_card: Card, val visible_cards: List ) ``` The [Kotlin documentation][moshi_kotlin_docs] explains the required build configuration changes. * New: Change how Moshi's generated adapters call constructors. Previous generated code used a combination of the constructor and `copy()` method to set properties that have default values. With this update we call the same synthetic constructor that Kotlin uses. This is less surprising though it requires us to generate some tricky code. * New: Make `Rfc3339DateJsonAdapter` null-safe. Previously Moshi would refuse to decode null dates. Restore that behavior by explicitly forbidding nulls with `Rfc3339DateJsonAdapter().nonNull()`. * New: Require Kotlin 1.3.50 or newer. * New: `JsonWriter.valueSink()` streams JSON-formatted data inline. Use this to do basic includes of raw JSON within a streamed document. * New: Support Gradle incremental processing in code gen. * New: Improve error messages. This includes better errors when field names and JSON names disagree, and when failing on an unknown field. * New: Support default values in `PolymorphicJsonAdapterFactory`. * New: Permit multiple labels for each subtype in `PolymorphicJsonAdapterFactory`. The first label is used when writing an object to JSON. * New: Forbid automatic encoding of platform classes in `kotlinx`. As with `java.*`, `android.*`, and `kotlin.*` Moshi wants you to specify how to encode platform types. * New: `@JsonClass(generator=...)` makes it possible for third-party libraries to provide generated adapters when Moshi's default adapters are insufficient. * Fix: Simplify wildcard types like `List` to their base types `List` when finding type adapters. This is especially useful with Kotlin where wildcards may be added automatically. * Fix: Use the correct name when the `@Json` annotation uses field targeting like `@field:Json`. * Fix: Support multiple transient properties in `KotlinJsonAdapter`. * Fix: Don't explode attempting to resolve self-referential type variables like in `Comparable>`. * Fix: Don't infinite loop on `skipValue()` at the end an object or array. Also disallow calling `skipValue()` at the end of a document. ## Version 1.8.0 _2018-11-09_ * New: Support JSON objects that include type information in the JSON. The new `PolymorphicJsonAdapterFactory` writes a type field when encoding, and reads it when decoding. * New: Fall back to the reflection-based `KotlinJsonAdapterFactory` if it is enabled and a generated adapter is not found. This makes it possible to use reflection-based JSON adapters in development (so you don't have to wait for code to be regenerated on every build) and generated JSON adapters in production (so you don't need the kotlin-reflect library). * New: The `peekJson()` method on `JsonReader` let you read ahead on a JSON stream without consuming it. This builds on Okio's new `Buffer.peek()` API. * New: The `beginFlatten()` and `endFlatten()` methods on `JsonWriter` suppress unwanted nesting when composing adapters. Previously it was necessary to flatten objects in memory before writing. * New: Upgrade to Okio 1.16.0. We don't yet require Kotlin-friendly Okio 2.1 but Moshi works fine with that release. ```kotlin implementation("com.squareup.okio:okio:1.16.0") ``` * Fix: Don't return partially-constructed adapters when using a Moshi instance concurrently. * Fix: Eliminate warnings and errors in generated `.kt` triggered by type variance, primitive types, and required values. * Fix: Improve the supplied rules (`moshi.pro`) to better retain symbols used by Moshi. We recommend R8 when shrinking code. * Fix: Remove code generation companion objects. This API was neither complete nor necessary. ## Version 1.7.0 _2018-09-24_ * New: `EnumJsonAdapter` makes it easy to specify a fallback value for unknown enum constants. By default Moshi throws an `JsonDataException` if it reads an unknown enum constant. With this you can specify a fallback value or null. ```java new Moshi.Builder() .add(EnumJsonAdapter.create(IsoCurrency.class) .withUnknownFallback(IsoCurrency.USD)) .build(); ``` Note that this adapter is in the optional `moshi-adapters` module. ```groovy implementation 'com.squareup.moshi:moshi-adapters:1.7.0' ``` * New: Embed R8/ProGuard rules in the `.jar` file. * New: Use `@CheckReturnValue` in more places. We hope this will encourage you to use `skipName()` instead of `nextName()` for better performance! * New: Forbid automatic encoding of platform classes in `androidx`. As with `java.*`, `android.*`, and `kotlin.*` Moshi wants you to specify how to encode platform types. * New: Improve error reporting when creating an adapter fails. * New: Upgrade to Okio 1.15.0. We don't yet require Kotlin-friendly Okio 2.x but Moshi works fine with that release. ```groovy implementation 'com.squareup.okio:okio:1.15.0' ``` * Fix: Return false from `JsonReader.hasNext()` at document's end. * Fix: Improve code gen to handle several broken cases. Our generated adapters had problems with nulls, nested parameterized types, private transient properties, generic type aliases, fields with dollar signs in their names, and named companion objects. ## Version 1.6.0 _2018-05-14_ * **Moshi now supports codegen for Kotlin.** We've added a new annotation processor that generates a small and fast JSON adapter for your Kotlin types. It can be used on its own or with the existing `KotlinJsonAdapterFactory` adapter. * **Moshi now resolves all type parameters.** Previously Moshi wouldn't resolve type parameters on top-level classes. * New: Support up to 255 levels of nesting when reading and writing JSON. Previously Moshi would reject JSON input that contained more than 32 levels of nesting. * New: Write encoded JSON to a stream with `JsonWriter.value(BufferedSource)`. Use this to emit a JSON value without decoding it first. * New: `JsonAdapter.nonNull()` returns a new JSON adapter that forbids explicit nulls in the JSON body. Use this to detect and fail eagerly on unwanted nulls. * New: `JsonReader.skipName()` is like `nextName()` but it avoids allocating when a name is unknown. Use this when `JsonReader.selectName()` returns -1. * New: Automatic module name of `com.squareup.moshi` for use with the Java Platform Module System. This moves moshi-adapters into its own `.adapters` package and forwards the existing adapter. It moves the moshi-kotlin into its own `.kotlin.reflect` package and forwards the existing adapter. * New: Upgrade to Okio 1.14.0. ```xml com.squareup.okio okio 1.14.0 com.squareup.okio:okio:1.14.0 ``` * Fix: Fail fast if there are trailing non-whitespace characters in the JSON passed to `JsonAdapter.fromJson(String)`. Previously such data was ignored! * Fix: Fail fast when Kotlin types are abstract, inner, or object instances. * Fix: Fail fast if `name()` is called out of sequence. * Fix: Handle asymmetric `Type.equals()` methods when doing type comparisons. Previously it was possible that a registered type adapter would not be used because its `Type.equals()` method was not consistent with a user-provided type. * Fix: `JsonValueReader.selectString()` now returns -1 for non-strings instead of throwing. * Fix: Permit reading numbers as strings when the `JsonReader` was created from a JSON value. This was always supported when reading from a stream but broken when reading from a decoded value. * Fix: Delegate to user-adapters in the adapter for Object.class. Previously when Moshi encountered an opaque Object it would only use the built-in adapters. With this change user-installed adapters for types like `String` will always be honored. ## Version 1.5.0 _2017-05-14_ * **Moshi now uses `@Nullable` to annotate all possibly-null values.** We've added a compile-time dependency on the JSR 305 annotations. This is a [provided][maven_provided] dependency and does not need to be included in your build configuration, `.jar` file, or `.apk`. We use `@ParametersAreNonnullByDefault` and all parameters and return types are never null unless explicitly annotated `@Nullable`. * **Warning: Moshi APIs in this update are source-incompatible for Kotlin callers.** Nullability was previously ambiguous and lenient but now the compiler will enforce strict null checks. * **Kotlin models are now supported via the `moshi-kotlin` extension.** `KotlinJsonAdapterFactory` is the best way to use Kotlin with Moshi. It honors default values and is null-safe. Kotlin users that don't use this factory should write custom adapters for their JSON types. Otherwise Moshi cannot properly initialize delegated properties of the objects it decodes. * New: Upgrade to Okio 1.13.0. ```xml com.squareup.okio okio 1.13.0 com.squareup.okio:okio:1.13.0 ``` * New: You may now declare delegates in `@ToJson` and `@FromJson` methods. If one of the arguments to the method is a `JsonAdapter` of the same type, that will be the next eligible adapter for that type. This may be useful for composing adapters. * New: `Types.equals(Type, Type)` makes it easier to compare types in `JsonAdapter.Factory`. * Fix: Retain the sign on negative zero. ## Version 1.4.0 _2017-02-04_ Moshi 1.4 is a major release that adds _JSON values_ as a core part of the library. We consider any Java object comprised of maps, lists, strings, numbers, booleans and nulls to be a JSON value. These are equivalent to parsed JSON objects in JavaScript, [Gson][gson]’s `JsonElement`, and [Jackson][jackson]’s `JsonNode`. Unlike Jackson and Gson, Moshi just uses Java’s built-in types for its values:
JSON typeJava type
{...}ObjectMap<String, Object>
[...]ArrayList<Object>
"abc"StringString
123NumberDouble, Long, or BigDecimal
trueBooleanBoolean
nullnullnull
Moshi's new API `JsonAdapter.toJsonValue()` converts your application classes to JSON values comprised of the above types. Symmetrically, `JsonAdapter.fromJsonValue()` converts JSON values to your application classes. * New: `JsonAdapter.toJsonValue()` and `fromJsonValue()`. * New: `JsonReader.readJsonValue()` reads a JSON value from a stream. * New: `Moshi.adapter(Type, Class)` lets you look up the adapter for a qualified type. * New: `JsonAdapter.serializeNulls()` and `indent()` return JSON adapters that customize the format of the encoded JSON. * New: `JsonReader.selectName()` and `selectString()` optimize decoding JSON with known names and values. * New: `Types.nextAnnotations()` reduces the amount of code required to implement a custom `JsonAdapter.Factory`. * Fix: Don't fail on large longs that have a fractional component like `9223372036854775806.0`. ## Version 1.3.1 _2016-10-21_ * Fix: Don't incorrectly report invalid input when a slash character is escaped. When we tightened our invalid escape handling we missed the one character that is valid both escaped `\/` and unescaped `/`. ## Version 1.3.0 _2016-10-15_ * New: Permit `@ToJson` and `@FromJson` methods to take any number of `JsonAdapter` parameters to delegate to. This is supported for `@ToJson` methods that take a `JsonWriter` and `@FromJson` methods that take a `JsonReader`. * New: Throw `JsonEncodingException` when the incoming data is not valid JSON. Use this to differentiate data format problems from connectivity problems. * New: Upgrade to Okio 1.11.0. ```xml com.squareup.okio okio 1.11.0 ``` * New: Omit Kotlin (`kotlin.*`) and Scala (`scala.*`) platform types when encoding objects using their fields. This should make it easier to avoid unexpected dependencies on platform versions. * Fix: Explicitly limit reading and writing to 31 levels of nested structure. Previously no specific limit was enforced, but deeply nested documents would fail with either an `ArrayIndexOutOfBoundsException` due to a bug in `JsonWriter`'s path management, or a `StackOverflowError` due to excessive recursion. * Fix: Require enclosed types to specify their enclosing type with `Types.newParameterizedTypeWithOwner()`. Previously this API did not exist and looking up adapters for enclosed parameterized types was not possible. * Fix: Fail on invalid escapes. Previously any character could be escaped. With this fix only characters permitted to be escaped may be escaped. Use `JsonReader.setLenient(true)` to read JSON documents that escape characters that should not be escaped. ## Version 1.2.0 _2016-05-28_ * New: Take advantage of Okio's new `Options` feature when reading field names and enum values. This has a significant impact on performance. We measured parsing performance improve from 89k ops/sec to 140k ops/sec on one benchmark on one machine. * New: Upgrade to Okio 1.8.0. ```xml com.squareup.okio okio 1.8.0 ``` * New: Support types that lack no-argument constructors objects on Android releases prior to Gingerbread. * Fix: Add writer value overload for boxed booleans. Autoboxing resolves boxed longs and doubles to `value(Number)`, but a boxed boolean would otherwise resolve to value(boolean) with an implicit call to booleanValue() which has the potential to throw NPEs. * Fix: Be more aggressive about canonicalizing types. ## Version 1.1.0 _2016-01-19_ * New: Support [RFC 7159][rfc_7159], the latest JSON specification. This removes the constraint that the root value must be an array or an object. It may now take any value: array, object, string, number, boolean, or null. Previously this was only permitted if the adapter was configured to be lenient. * New: Enum constants may be annotated with `@Json` to customize their encoded value. * New: Create new builder from Moshi instance with `Moshi.newBuilder()`. * New: `Types.getRawType()` and `Types.collectionElementType()` APIs to assist in defining generic type adapter factories. ## Version 1.0.0 _2015-09-27_ * **API Change**: Replaced `new JsonReader()` with `JsonReader.of()` and `new JsonWriter()` with `JsonWriter.of()`. If your code calls either of these constructors it will need to be updated to call the static factory method instead. * **API Change**: Don’t throw `IOException` on `JsonAdapter.toJson(T)`. Code that calls this method may need to be fixed to no longer catch an impossible `IOException`. * Fix: the JSON adapter for `Object` no longer fails when encountering `null` in the stream. * New: `@Json` annotation can customize a field's name. This is particularly handy for fields whose names are Java keywords, like `default` or `public`. * New: `Rfc3339DateJsonAdapter` converts between a `java.util.Date` and a string formatted with RFC 3339 (like `2015-09-26T18:23:50.250Z`). This class is in the new `moshi-adapters` subproject. You will need to register this adapter if you want this date formatting behavior. See it in action in the [dates example][dates_example]. * New: `Moshi.adapter()` keeps a cache of all created adapters. For best efficiency, application code should keep a reference to required adapters in a field. * New: The `Types` factory class makes it possible to compose types like `List` or `Map`. This is useful to look up JSON adapters for parameterized types. * New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an unknown value is encountered on the stream. Use this in development and debug builds to detect typos in field names. This feature shouldn’t be used in production because it makes migrations very difficult. ## Version 0.9.0 _2015-06-16_ * Databinding for primitive types, strings, enums, arrays, collections, and maps. * Databinding for plain old Java objects. * [JSONPath](http://goessner.net/articles/JsonPath/) support for both `JsonReader` and `JsonWriter`. * Throw `JsonDataException` when there’s a data binding problem. * Adapter methods: `@ToJson` and `@FromJson`. * Qualifier annotations: `@JsonQualifier` to permit different type adapters for the same Java type. * Imported code from Gson: `JsonReader`, `JsonWriter`. Also some internal classes: `LinkedHashTreeMap` for hash-collision avoidance and `Types` for typesafe databinding. [dates_example]: https://github.com/square/moshi/blob/master/examples/src/main/java/com/squareup/moshi/recipes/ReadAndWriteRfc3339Dates.java [gson]: https://github.com/google/gson [jackson]: http://wiki.fasterxml.com/JacksonHome [kotlin_1_4_10]: https://github.com/JetBrains/kotlin/releases/tag/v1.4.10 [kotlin_1_4_31]: https://github.com/JetBrains/kotlin/releases/tag/v1.4.31 [kotlin_1_6_0]: https://github.com/JetBrains/kotlin/releases/tag/v1.6.0 [kotlin_1_7_0]: https://github.com/JetBrains/kotlin/releases/tag/v1.7.0 [kotlinpoet_1_12_0]: https://github.com/square/kotlinpoet/releases/tag/1.12.0 [kotlinx_metadata_0_5_0]: https://github.com/JetBrains/kotlin/blob/master/libraries/kotlinx-metadata/jvm/ChangeLog.md#050 [ksp]: https://github.com/google/ksp [ksp_1_7_0_1_0_6]: https://github.com/google/ksp/releases/tag/1.7.10-1.0.6 [maven_provided]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html [moshi_kotlin_docs]: https://github.com/square/moshi/blob/master/README.md#kotlin [mrjar]: https://openjdk.java.net/jeps/238 [okio_1_7_5]: https://square.github.io/okio/changelog/#version-1175 [okio_2_10_0]: https://square.github.io/okio/changelog/#version-2100 [rfc_7159]: https://tools.ietf.org/html/rfc7159 ================================================ FILE: CONTRIBUTING.md ================================================ Contributing ============ If you would like to contribute code to Moshi you can do so through GitHub by forking the repository and sending a pull request. When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. Please also make sure your code compiles by running `./gradlew build`. For any formatting errors, run `./gradlew spotlessApply` to fix them. Before your code can be accepted into the project you must also sign the [Individual Contributor License Agreement (CLA)][1]. [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 ================================================ FILE: LICENSE.txt ================================================ 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 [yyyy] [name of copyright owner] 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 ===== Moshi is a modern JSON library for Android, Java and Kotlin. It makes it easy to parse JSON into Java and Kotlin classes: _Note: The Kotlin examples of this README assume use of either Kotlin code gen or `KotlinJsonAdapterFactory` for reflection. Plain Java-based reflection is unsupported on Kotlin classes._
Java ```java String json = ...; Moshi moshi = new Moshi.Builder().build(); JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class); BlackjackHand blackjackHand = jsonAdapter.fromJson(json); System.out.println(blackjackHand); ```
Kotlin ```kotlin val json: String = ... val moshi: Moshi = Moshi.Builder().build() val jsonAdapter: JsonAdapter = moshi.adapter() val blackjackHand = jsonAdapter.fromJson(json) println(blackjackHand) ```
And it can just as easily serialize Java or Kotlin objects as JSON:
Java ```java BlackjackHand blackjackHand = new BlackjackHand( new Card('6', SPADES), Arrays.asList(new Card('4', CLUBS), new Card('A', HEARTS))); Moshi moshi = new Moshi.Builder().build(); JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class); String json = jsonAdapter.toJson(blackjackHand); System.out.println(json); ```
Kotlin ```kotlin val blackjackHand = BlackjackHand( Card('6', SPADES), listOf(Card('4', CLUBS), Card('A', HEARTS)) ) val moshi: Moshi = Moshi.Builder().build() val jsonAdapter: JsonAdapter = moshi.adapter() val json: String = jsonAdapter.toJson(blackjackHand) println(json) ```
### Built-in Type Adapters Moshi has built-in support for reading and writing Java’s core data types: * Primitives (int, float, char...) and their boxed counterparts (Integer, Float, Character...). * Arrays, Collections, Lists, Sets, and Maps * Strings * Enums It supports your model classes by writing them out field-by-field. In the example above Moshi uses these classes:
Java ```java class BlackjackHand { public final Card hidden_card; public final List visible_cards; ... } class Card { public final char rank; public final Suit suit; ... } enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES; } ```
Kotlin ```kotlin class BlackjackHand( val hidden_card: Card, val visible_cards: List, ... ) class Card( val rank: Char, val suit: Suit ... ) enum class Suit { CLUBS, DIAMONDS, HEARTS, SPADES; } ```
to read and write this JSON: ```json { "hidden_card": { "rank": "6", "suit": "SPADES" }, "visible_cards": [ { "rank": "4", "suit": "CLUBS" }, { "rank": "A", "suit": "HEARTS" } ] } ``` The [Javadoc][javadoc] catalogs the complete Moshi API, which we explore below. ### Custom Type Adapters With Moshi, it’s particularly easy to customize how values are converted to and from JSON. A type adapter is any class that has methods annotated `@ToJson` and `@FromJson`. For example, Moshi’s default encoding of a playing card is verbose: the JSON defines the rank and suit in separate fields: `{"rank":"A","suit":"HEARTS"}`. With a type adapter, we can change the encoding to something more compact: `"4H"` for the four of hearts or `"JD"` for the jack of diamonds:
Java ```java class CardAdapter { @ToJson String toJson(Card card) { return card.rank + card.suit.name().substring(0, 1); } @FromJson Card fromJson(String card) { if (card.length() != 2) throw new JsonDataException("Unknown card: " + card); char rank = card.charAt(0); switch (card.charAt(1)) { case 'C': return new Card(rank, Suit.CLUBS); case 'D': return new Card(rank, Suit.DIAMONDS); case 'H': return new Card(rank, Suit.HEARTS); case 'S': return new Card(rank, Suit.SPADES); default: throw new JsonDataException("unknown suit: " + card); } } } ```
Kotlin ```kotlin class CardAdapter { @ToJson fun toJson(card: Card): String { return card.rank + card.suit.name.substring(0, 1) } @FromJson fun fromJson(card: String): Card { if (card.length != 2) throw JsonDataException("Unknown card: $card") val rank = card[0] return when (card[1]) { 'C' -> Card(rank, Suit.CLUBS) 'D' -> Card(rank, Suit.DIAMONDS) 'H' -> Card(rank, Suit.HEARTS) 'S' -> Card(rank, Suit.SPADES) else -> throw JsonDataException("unknown suit: $card") } } } ```
Register the type adapter with the `Moshi.Builder` and we’re good to go.
Java ```java Moshi moshi = new Moshi.Builder() .add(new CardAdapter()) .build(); ```
Kotlin ```kotlin val moshi = Moshi.Builder() .add(CardAdapter()) .build() ```
Voilà: ```json { "hidden_card": "6S", "visible_cards": [ "4C", "AH" ] } ``` #### Another example Note that the method annotated with `@FromJson` does not need to take a String as an argument. Rather it can take input of any type and Moshi will first parse the JSON to an object of that type and then use the `@FromJson` method to produce the desired final value. Conversely, the method annotated with `@ToJson` does not have to produce a String. Assume, for example, that we have to parse a JSON in which the date and time of an event are represented as two separate strings. ```json { "title": "Blackjack tournament", "begin_date": "20151010", "begin_time": "17:04" } ``` We would like to combine these two fields into one string to facilitate the date parsing at a later point. Also, we would like to have all variable names in CamelCase. Therefore, the `Event` class we want Moshi to produce like this:
Java ```java class Event { String title; String beginDateAndTime; } ```
Kotlin ```kotlin class Event( val title: String, val beginDateAndTime: String ) ```
Instead of manually parsing the JSON line per line (which we could also do) we can have Moshi do the transformation automatically. We simply define another class `EventJson` that directly corresponds to the JSON structure:
Java ```java class EventJson { String title; String begin_date; String begin_time; } ```
Kotlin ```kotlin class EventJson( val title: String, val begin_date: String, val begin_time: String ) ```
And another class with the appropriate `@FromJson` and `@ToJson` methods that are telling Moshi how to convert an `EventJson` to an `Event` and back. Now, whenever we are asking Moshi to parse a JSON to an `Event` it will first parse it to an `EventJson` as an intermediate step. Conversely, to serialize an `Event` Moshi will first create an `EventJson` object and then serialize that object as usual.
Java ```java class EventJsonAdapter { @FromJson Event eventFromJson(EventJson eventJson) { Event event = new Event(); event.title = eventJson.title; event.beginDateAndTime = eventJson.begin_date + " " + eventJson.begin_time; return event; } @ToJson EventJson eventToJson(Event event) { EventJson json = new EventJson(); json.title = event.title; json.begin_date = event.beginDateAndTime.substring(0, 8); json.begin_time = event.beginDateAndTime.substring(9, 14); return json; } } ```
Kotlin ```kotlin class EventJsonAdapter { @FromJson fun eventFromJson(eventJson: EventJson): Event { return Event( title = eventJson.title, beginDateAndTime = "${eventJson.begin_date} ${eventJson.begin_time}" ) } @ToJson fun eventToJson(event: Event): EventJson { return EventJson( title = event.title, begin_date = event.beginDateAndTime.substring(0, 8), begin_time = event.beginDateAndTime.substring(9, 14), ) } } ```
Again we register the adapter with Moshi.
Java ```java Moshi moshi = new Moshi.Builder() .add(new EventJsonAdapter()) .build(); ```
Kotlin ```kotlin val moshi = Moshi.Builder() .add(EventJsonAdapter()) .build() ```
We can now use Moshi to parse the JSON directly to an `Event`.
Java ```java JsonAdapter jsonAdapter = moshi.adapter(Event.class); Event event = jsonAdapter.fromJson(json); ```
Kotlin ```kotlin val jsonAdapter = moshi.adapter() val event = jsonAdapter.fromJson(json) ```
### Adapter convenience methods Moshi provides a number of convenience methods for `JsonAdapter` objects: - `nullSafe()` - `nonNull()` - `lenient()` - `failOnUnknown()` - `indent()` - `serializeNulls()` These factory methods wrap an existing `JsonAdapter` into additional functionality. For example, if you have an adapter that doesn't support nullable values, you can use `nullSafe()` to make it null safe:
Java ```java String dateJson = "\"2018-11-26T11:04:19.342668Z\""; String nullDateJson = "null"; // Hypothetical IsoDateDapter, doesn't support null by default JsonAdapter adapter = new IsoDateDapter(); Date date = adapter.fromJson(dateJson); System.out.println(date); // Mon Nov 26 12:04:19 CET 2018 Date nullDate = adapter.fromJson(nullDateJson); // Exception, com.squareup.moshi.JsonDataException: Expected a string but was NULL at path $ Date nullDate = adapter.nullSafe().fromJson(nullDateJson); System.out.println(nullDate); // null ```
Kotlin ```kotlin val dateJson = "\"2018-11-26T11:04:19.342668Z\"" val nullDateJson = "null" // Hypothetical IsoDateDapter, doesn't support null by default val adapter: JsonAdapter = IsoDateDapter() val date = adapter.fromJson(dateJson) println(date) // Mon Nov 26 12:04:19 CET 2018 val nullDate = adapter.fromJson(nullDateJson) // Exception, com.squareup.moshi.JsonDataException: Expected a string but was NULL at path $ val nullDate = adapter.nullSafe().fromJson(nullDateJson) println(nullDate) // null ```
In contrast to `nullSafe()` there is `nonNull()` to make an adapter refuse null values. Refer to the Moshi JavaDoc for details on the various methods. ### Parse JSON Arrays Say we have a JSON string of this structure: ```json [ { "rank": "4", "suit": "CLUBS" }, { "rank": "A", "suit": "HEARTS" } ] ``` We can now use Moshi to parse the JSON string into a `List`.
Java ```java String cardsJsonResponse = ...; Type type = Types.newParameterizedType(List.class, Card.class); JsonAdapter> adapter = moshi.adapter(type); List cards = adapter.fromJson(cardsJsonResponse); ```
Kotlin ```kotlin val cardsJsonResponse: String = ... // We can just use a reified extension! val adapter = moshi.adapter>() val cards: List = adapter.fromJson(cardsJsonResponse) ```
### Fails Gracefully Automatic databinding almost feels like magic. But unlike the black magic that typically accompanies reflection, Moshi is designed to help you out when things go wrong. ``` JsonDataException: Expected one of [CLUBS, DIAMONDS, HEARTS, SPADES] but was ANCHOR at path $.visible_cards[2].suit at com.squareup.moshi.JsonAdapters$11.fromJson(JsonAdapters.java:188) at com.squareup.moshi.JsonAdapters$11.fromJson(JsonAdapters.java:180) ... ``` Moshi always throws a standard `java.io.IOException` if there is an error reading the JSON document, or if it is malformed. It throws a `JsonDataException` if the JSON document is well-formed, but doesn’t match the expected format. ### Built on Okio Moshi uses [Okio][okio] for simple and powerful I/O. It’s a fine complement to [OkHttp][okhttp], which can share buffer segments for maximum efficiency. ### Borrows from Gson Moshi uses the same streaming and binding mechanisms as [Gson][gson]. If you’re a Gson user you’ll find Moshi works similarly. If you try Moshi and don’t love it, you can even migrate to Gson without much violence! But the two libraries have a few important differences: * **Moshi has fewer built-in type adapters.** For example, you need to configure your own date adapter. Most binding libraries will encode whatever you throw at them. Moshi refuses to serialize platform types (`java.*`, `javax.*`, and `android.*`) without a user-provided type adapter. This is intended to prevent you from accidentally locking yourself to a specific JDK or Android release. * **Moshi is less configurable.** There’s no field naming strategy, versioning, instance creators, or long serialization policy. Instead of naming a field `visibleCards` and using a policy class to convert that to `visible_cards`, Moshi wants you to just name the field `visible_cards` as it appears in the JSON. * **Moshi doesn’t have a `JsonElement` model.** Instead it just uses built-in types like `List` and `Map`. * **No HTML-safe escaping.** Gson encodes `=` as `\u003d` by default so that it can be safely encoded in HTML without additional escaping. Moshi encodes it naturally (as `=`) and assumes that the HTML encoder – if there is one – will do its job. ### Custom field names with @Json Moshi works best when your JSON objects and Java or Kotlin classes have the same structure. But when they don't, Moshi has annotations to customize data binding. Use `@Json` to specify how Java fields or Kotlin properties map to JSON names. This is necessary when the JSON name contains spaces or other characters that aren’t permitted in Java field or Kotlin property names. For example, this JSON has a field name containing a space: ```json { "username": "jesse", "lucky number": 32 } ``` With `@Json` its corresponding Java or Kotlin class is easy:
Java ```java class Player { String username; @Json(name = "lucky number") int luckyNumber; ... } ```
Kotlin ```kotlin class Player { val username: String @Json(name = "lucky number") val luckyNumber: Int ... } ```
Because JSON field names are always defined with their Java or Kotlin fields, Moshi makes it easy to find fields when navigating between Java or Koltin and JSON. ### Alternate type adapters with @JsonQualifier Use `@JsonQualifier` to customize how a type is encoded for some fields without changing its encoding everywhere. This works similarly to the qualifier annotations in dependency injection tools like Dagger and Guice. Here’s a JSON message with two integers and a color: ```json { "width": 1024, "height": 768, "color": "#ff0000" } ``` By convention, Android programs also use `int` for colors:
Java ```java class Rectangle { int width; int height; int color; } ```
Kotlin ```kotlin class Rectangle( val width: Int, val height: Int, val color: Int ) ```
But if we encoded the above Java or Kotlin class as JSON, the color isn't encoded properly! ```json { "width": 1024, "height": 768, "color": 16711680 } ``` The fix is to define a qualifier annotation, itself annotated `@JsonQualifier`:
Java ```java @Retention(RUNTIME) @JsonQualifier public @interface HexColor { } ```
Kotlin ```kotlin @Retention(RUNTIME) @JsonQualifier annotation class HexColor ```
Next apply this `@HexColor` annotation to the appropriate field:
Java ```java class Rectangle { int width; int height; @HexColor int color; } ```
Kotlin ```kotlin class Rectangle( val width: Int, val height: Int, @HexColor val color: Int ) ```
And finally define a type adapter to handle it:
Java ```java /** Converts strings like #ff0000 to the corresponding color ints. */ class ColorAdapter { @ToJson String toJson(@HexColor int rgb) { return String.format("#%06x", rgb); } @FromJson @HexColor int fromJson(String rgb) { return Integer.parseInt(rgb.substring(1), 16); } } ```
Kotlin ```kotlin /** Converts strings like #ff0000 to the corresponding color ints. */ class ColorAdapter { @ToJson fun toJson(@HexColor rgb: Int): String { return "#%06x".format(rgb) } @FromJson @HexColor fun fromJson(rgb: String): Int { return rgb.substring(1).toInt(16) } } ```
Use `@JsonQualifier` when you need different JSON encodings for the same type. Most programs shouldn’t need this `@JsonQualifier`, but it’s very handy for those that do. ### Omitting fields Some models declare fields that shouldn’t be included in JSON. For example, suppose our blackjack hand has a `total` field with the sum of the cards:
Java ```java public final class BlackjackHand { private int total; ... } ```
Kotlin ```kotlin class BlackjackHand( private val total: Int, ... ) ```
By default, all fields are emitted when encoding JSON, and all fields are accepted when decoding JSON. Prevent a field from being included by annotating them with `@Json(ignore = true)`.
Java ```java public final class BlackjackHand { @Json(ignore = true) private int total; ... } ```
Kotlin ```kotlin class BlackjackHand(...) { @Json(ignore = true) var total: Int = 0 ... } ```
These fields are omitted when writing JSON. When reading JSON, the field is skipped even if the JSON contains a value for the field. Instead, it will get a default value. In Kotlin, these fields _must_ have a default value if they are in the primary constructor. Note that you can also use Java’s `transient` keyword or Kotlin's `@Transient` annotation on these fields for the same effect. ### Default Values & Constructors When reading JSON that is missing a field, Moshi relies on the Java or Kotlin or Android runtime to assign the field’s value. Which value it uses depends on whether the class has a no-arguments constructor. If the class has a no-arguments constructor, Moshi will call that constructor and whatever value it assigns will be used. For example, because this class has a no-arguments constructor the `total` field is initialized to `-1`. Note: This section only applies to Java reflections. ```java public final class BlackjackHand { private int total = -1; ... private BlackjackHand() { } public BlackjackHand(Card hidden_card, List visible_cards) { ... } } ``` If the class doesn’t have a no-arguments constructor, Moshi can’t assign the field’s default value, **even if it’s specified in the field declaration**. Instead, the field’s default is always `0` for numbers, `false` for booleans, and `null` for references. In this example, the default value of `total` is `0`! ```java public final class BlackjackHand { private int total = -1; ... public BlackjackHand(Card hidden_card, List visible_cards) { ... } } ``` This is surprising and is a potential source of bugs! For this reason consider defining a no-arguments constructor in classes that you use with Moshi, using `@SuppressWarnings("unused")` to prevent it from being inadvertently deleted later: ```java public final class BlackjackHand { private int total = -1; ... @SuppressWarnings("unused") // Moshi uses this! private BlackjackHand() { } public BlackjackHand(Card hidden_card, List visible_cards) { ... } } ``` ### Composing Adapters In some situations Moshi's default Java-to-JSON conversion isn't sufficient. You can compose adapters to build upon the standard conversion. In this example, we turn serialize nulls, then delegate to the built-in adapter:
Java ```java class TournamentWithNullsAdapter { @ToJson void toJson(JsonWriter writer, Tournament tournament, JsonAdapter delegate) throws IOException { boolean wasSerializeNulls = writer.getSerializeNulls(); writer.setSerializeNulls(true); try { delegate.toJson(writer, tournament); } finally { writer.setLenient(wasSerializeNulls); } } } ```
Kotlin ```kotlin class TournamentWithNullsAdapter { @ToJson fun toJson(writer: JsonWriter, tournament: Tournament?, delegate: JsonAdapter) { val wasSerializeNulls: Boolean = writer.getSerializeNulls() writer.setSerializeNulls(true) try { delegate.toJson(writer, tournament) } finally { writer.setLenient(wasSerializeNulls) } } } ```
When we use this to serialize a tournament, nulls are written! But nulls elsewhere in our JSON document are skipped as usual. Moshi has a powerful composition system in its `JsonAdapter.Factory` interface. We can hook in to the encoding and decoding process for any type, even without knowing about the types beforehand. In this example, we customize types annotated `@AlwaysSerializeNulls`, which an annotation we create, not built-in to Moshi:
Java ```java @Target(TYPE) @Retention(RUNTIME) public @interface AlwaysSerializeNulls {} ```
Kotlin ```kotlin @Target(TYPE) @Retention(RUNTIME) annotation class AlwaysSerializeNulls ```
Java ```java @AlwaysSerializeNulls static class Car { String make; String model; String color; } ```
Kotlin ```kotlin @AlwaysSerializeNulls class Car( val make: String?, val model: String?, val color: String? ) ```
Each `JsonAdapter.Factory` interface is invoked by `Moshi` when it needs to build an adapter for a user's type. The factory either returns an adapter to use, or null if it doesn't apply to the requested type. In our case we match all classes that have our annotation.
Java ```java static class AlwaysSerializeNullsFactory implements JsonAdapter.Factory { @Override public JsonAdapter create( Type type, Set annotations, Moshi moshi) { Class rawType = Types.getRawType(type); if (!rawType.isAnnotationPresent(AlwaysSerializeNulls.class)) { return null; } JsonAdapter delegate = moshi.nextAdapter(this, type, annotations); return delegate.serializeNulls(); } } ```
Kotlin ```kotlin class AlwaysSerializeNullsFactory : JsonAdapter.Factory { override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { val rawType: Class<*> = type.rawType if (!rawType.isAnnotationPresent(AlwaysSerializeNulls::class.java)) { return null } val delegate: JsonAdapter = moshi.nextAdapter(this, type, annotations) return delegate.serializeNulls() } } ```
After determining that it applies, the factory looks up Moshi's built-in adapter by calling `Moshi.nextAdapter()`. This is key to the composition mechanism: adapters delegate to each other! The composition in this example is simple: it applies the `serializeNulls()` transform on the delegate. Composing adapters can be very sophisticated: * An adapter could transform the input object before it is JSON-encoded. A string could be trimmed or truncated; a value object could be simplified or normalized. * An adapter could repair the output object after it is JSON-decoded. It could fill-in missing data or discard unwanted data. * The JSON could be given extra structure, such as wrapping values in objects or arrays. Moshi is itself built on the pattern of repeatedly composing adapters. For example, Moshi's built-in adapter for `List` delegates to the adapter of `T`, and calls it repeatedly. ### Precedence Moshi's composition mechanism tries to find the best adapter for each type. It starts with the first adapter or factory registered with `Moshi.Builder.add()`, and proceeds until it finds an adapter for the target type. If a type can be matched multiple adapters, the earliest one wins. To register an adapter at the end of the list, use `Moshi.Builder.addLast()` instead. This is most useful when registering general-purpose adapters, such as the `KotlinJsonAdapterFactory` below. Kotlin ------ Moshi is a great JSON library for Kotlin. It understands Kotlin’s non-nullable types and default parameter values. When you use Kotlin with Moshi you may use reflection, codegen, or both. #### Reflection The reflection adapter uses Kotlin’s reflection library to convert your Kotlin classes to and from JSON. Enable it by adding the `KotlinJsonAdapterFactory` to your `Moshi.Builder`: ```kotlin val moshi = Moshi.Builder() .addLast(KotlinJsonAdapterFactory()) .build() ``` Moshi’s adapters are ordered by precedence, so you should use `addLast()` with `KotlinJsonAdapterFactory`, and `add()` with your custom adapters. The reflection adapter requires the following additional dependency: ```xml com.squareup.moshi moshi-kotlin 1.15.2 ``` ```kotlin implementation("com.squareup.moshi:moshi-kotlin:2.0.0-alpha.1") ``` Note that the reflection adapter transitively depends on the `kotlin-reflect` library which is a 2.5 MiB .jar file. #### Codegen Moshi’s Kotlin codegen support can be used as a Kotlin SymbolProcessor ([KSP][ksp]). It generates a small and fast adapter for each of your Kotlin classes at compile-time. Enable it by annotating each class that you want to encode as JSON: ```kotlin @JsonClass(generateAdapter = true) data class BlackjackHand( val hidden_card: Card, val visible_cards: List ) ``` The codegen adapter requires that your Kotlin types and their properties be either `internal` or `public` (this is Kotlin’s default visibility). Kotlin codegen has no additional runtime dependency. You’ll need to enable kapt or KSP and then add the following to your build to enable the annotation processor:
KSP ```kotlin plugins { id("com.google.devtools.ksp") version "2.3.4" // Or latest version of KSP } dependencies { ksp("com.squareup.moshi:moshi-kotlin-codegen:2.0.0-alpha.1") } ```
#### Limitations If your Kotlin class has a superclass, it must also be a Kotlin class. Neither reflection or codegen support Kotlin types with Java supertypes or Java types with Kotlin supertypes. If you need to convert such classes to JSON you must create a custom type adapter. The JSON encoding of Kotlin types is the same whether using reflection or codegen. Prefer codegen for better performance and to avoid the `kotlin-reflect` dependency; prefer reflection to convert both private and protected properties. If you have configured both, generated adapters will be used on types that are annotated `@JsonClass(generateAdapter = true)`. Download -------- Download [the latest JAR][dl] or depend via Maven: ```xml com.squareup.moshi moshi 1.15.2 ``` or Gradle: ```kotlin implementation("com.squareup.moshi:moshi:2.0.0-alpha.1") ``` Snapshots of the development version are available in [the Central Portal Snapshots repository][snap]. R8 / ProGuard -------- Moshi contains minimally required rules for its own internals to work without need for consumers to embed their own. However if you are using reflective serialization and R8 or ProGuard, you must add keep rules in your proguard configuration file for your reflectively serialized classes. #### Enums Annotate enums with `@JsonClass(generateAdapter = false)` to prevent them from being removed/obfuscated from your code by R8/ProGuard. License -------- Copyright 2015 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. [dl]: https://search.maven.org/classic/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST [snap]: https://central.sonatype.com/repository/maven-snapshots/ [okio]: https://github.com/square/okio/ [okhttp]: https://github.com/square/okhttp/ [gson]: https://github.com/google/gson/ [javadoc]: https://square.github.io/moshi/1.x/moshi/ [ksp]: https://github.com/google/ksp ================================================ FILE: build.gradle.kts ================================================ import com.diffplug.gradle.spotless.JavaExtension import com.vanniktech.maven.publish.MavenPublishBaseExtension import java.net.URI import org.jetbrains.dokka.gradle.DokkaExtension import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension import org.jetbrains.kotlin.gradle.tasks.KotlinCompile buildscript { dependencies { val kotlinVersion = System.getenv("MOSHI_KOTLIN_VERSION") ?: libs.versions.kotlin.get() val kspVersion = System.getenv("MOSHI_KSP_VERSION") ?: libs.versions.ksp.get() classpath(kotlin("gradle-plugin", version = kotlinVersion)) classpath("com.google.devtools.ksp:symbol-processing-gradle-plugin:$kspVersion") } } plugins { alias(libs.plugins.mavenPublish) apply false alias(libs.plugins.dokka) alias(libs.plugins.spotless) alias(libs.plugins.japicmp) apply false alias(libs.plugins.ksp) apply false } allprojects { group = "com.squareup.moshi" version = project.property("VERSION_NAME") as String repositories { mavenCentral() } } spotless { format("misc") { target("*.md", ".gitignore") trimTrailingWhitespace() leadingTabsToSpaces(2) endWithNewline() } val configureCommonJavaFormat: JavaExtension.() -> Unit = { googleJavaFormat(libs.googleJavaFormat.get().version) } java { configureCommonJavaFormat() target("**/*.java") targetExclude("**/build/**") } kotlin { ktfmt(libs.ktfmt.get().version).googleStyle() target("**/*.kt") trimTrailingWhitespace() endWithNewline() targetExclude("**/Dependencies.kt", "**/build/**") } kotlinGradle { ktfmt(libs.ktfmt.get().version).googleStyle() target("**/*.gradle.kts") trimTrailingWhitespace() endWithNewline() } } subprojects { // Apply with "java" instead of just "java-library" so kotlin projects get it too pluginManager.withPlugin("java") { configure { toolchain { languageVersion.set(libs.versions.jdk.map(JavaLanguageVersion::of)) } } if (project.name != "records-tests") { tasks.withType().configureEach { options.release.set(8) } } } pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { tasks.withType().configureEach { compilerOptions { progressiveMode.set(true) jvmTarget.set(JvmTarget.fromTarget(libs.versions.jvmTarget.get())) } } configure { if (project.name != "examples") { explicitApi() } } } } dependencies { dokka(project(":moshi")) dokka(project(":moshi-adapters")) dokka(project(":moshi-kotlin")) dokka(project(":moshi-kotlin-codegen")) } dokka { dokkaPublications.html { outputDirectory.set(layout.projectDirectory.dir("docs/2.x")) } } subprojects { plugins.withId("org.jetbrains.dokka") { configure { basePublicationsDirectory.set(layout.buildDirectory.dir("dokkaDir")) dokkaSourceSets.configureEach { skipDeprecated.set(true) reportUndocumented.set(true) jdkVersion.set(8) perPackageOption { matchingRegex.set("com\\.squareup\\.moshi\\.internal.*") suppress.set(true) } externalDocumentationLinks.register("Okio") { packageListUrl("https://square.github.io/okio/3.x/okio/okio/package-list") url("https://square.github.io/okio/3.x/okio") } sourceLink { localDirectory.set(layout.projectDirectory.dir("src")) val relPath = rootProject.isolated.projectDirectory.asFile.toPath().relativize(projectDir.toPath()) remoteUrl("https://github.com/square/moshi/tree/main/$relPath/src") remoteLineSuffix.set("#L") } } } } plugins.withId("com.vanniktech.maven.publish") { configure { repositories { /* * Want to push to an internal repository for testing? * Set the following properties in ~/.gradle/gradle.properties. * * internalUrl=YOUR_INTERNAL_URL * internalUsername=YOUR_USERNAME * internalPassword=YOUR_PASSWORD * * Then run the following command to publish a new internal release: * * ./gradlew publishAllPublicationsToInternalRepository -DRELEASE_SIGNING_ENABLED=false */ val internalUrl = providers.gradleProperty("internalUrl").orNull if (internalUrl != null) { maven { name = "internal" url = URI(internalUrl) credentials { username = providers.gradleProperty("internalUsername").get() password = providers.gradleProperty("internalPassword").get() } } } } } configure { publishToMavenCentral(automaticRelease = true) signAllPublications() pom { description.set("A modern JSON API for Android and Java") name.set(project.name) url.set("https://github.com/square/moshi/") licenses { license { name.set("The Apache Software License, Version 2.0") url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") distribution.set("repo") } } scm { url.set("https://github.com/square/moshi/") connection.set("scm:git:git://github.com/square/moshi.git") developerConnection.set("scm:git:ssh://git@github.com/square/moshi.git") } developers { developer { id.set("square") name.set("Square, Inc.") } } } } } } ================================================ FILE: examples/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") alias(libs.plugins.ksp) } dependencies { ksp(project(":moshi-kotlin-codegen")) compileOnly(libs.jsr305) implementation(project(":moshi")) implementation(project(":moshi-adapters")) } tasks.withType().configureEach { compilerOptions { freeCompilerArgs.add("-opt-in=kotlin.ExperimentalStdlibApi") } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/ByteStrings.java ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import java.io.IOException; import okio.ByteString; public final class ByteStrings { public void run() throws Exception { String json = "\"TW9zaGksIE9saXZlLCBXaGl0ZSBDaGluPw\""; Moshi moshi = new Moshi.Builder().add(ByteString.class, new Base64ByteStringAdapter()).build(); JsonAdapter jsonAdapter = moshi.adapter(ByteString.class); ByteString byteString = jsonAdapter.fromJson(json); System.out.println(byteString); } /** * Formats byte strings using Base64. No line * breaks or whitespace is included in the encoded form. */ public final class Base64ByteStringAdapter extends JsonAdapter { @Override public ByteString fromJson(JsonReader reader) throws IOException { String base64 = reader.nextString(); return ByteString.decodeBase64(base64); } @Override public void toJson(JsonWriter writer, ByteString value) throws IOException { String string = value.base64(); writer.value(string); } } public static void main(String[] args) throws Exception { new ByteStrings().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/CardAdapter.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.FromJson; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.ToJson; import com.squareup.moshi.recipes.models.Card; import com.squareup.moshi.recipes.models.Suit; public final class CardAdapter { @ToJson String toJson(Card card) { return card.rank + card.suit.name().substring(0, 1); } @FromJson Card fromJson(String card) { if (card.length() != 2) throw new JsonDataException("Unknown card: " + card); char rank = card.charAt(0); switch (card.charAt(1)) { case 'C': return new Card(rank, Suit.CLUBS); case 'D': return new Card(rank, Suit.DIAMONDS); case 'H': return new Card(rank, Suit.HEARTS); case 'S': return new Card(rank, Suit.SPADES); default: throw new JsonDataException("unknown suit: " + card); } } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/CustomAdapterFactory.java ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.recipes; 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.annotation.Annotation; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import javax.annotation.Nullable; public final class CustomAdapterFactory { public void run() throws Exception { Moshi moshi = new Moshi.Builder().add(new SortedSetAdapterFactory()).build(); JsonAdapter> jsonAdapter = moshi.adapter(Types.newParameterizedType(SortedSet.class, String.class)); TreeSet model = new TreeSet<>(); model.add("a"); model.add("b"); model.add("c"); String json = jsonAdapter.toJson(model); System.out.println(json); } /** * This class composes an adapter for any element type into an adapter for a sorted set of those * elements. For example, given a {@code JsonAdapter}, use this to get a {@code * JsonAdapter>}. It works by looping over the input elements when both * reading and writing. */ static final class SortedSetAdapter extends JsonAdapter> { private final JsonAdapter elementAdapter; SortedSetAdapter(JsonAdapter elementAdapter) { this.elementAdapter = elementAdapter; } @Override public SortedSet fromJson(JsonReader reader) throws IOException { TreeSet result = new TreeSet<>(); reader.beginArray(); while (reader.hasNext()) { result.add(elementAdapter.fromJson(reader)); } reader.endArray(); return result; } @Override public void toJson(JsonWriter writer, SortedSet set) throws IOException { writer.beginArray(); for (T element : set) { elementAdapter.toJson(writer, element); } writer.endArray(); } } /** * Moshi asks this class to create JSON adapters. It only knows how to create JSON adapters for * {@code SortedSet} types, so it returns null for all other requests. When it does get a request * for a {@code SortedSet}, it asks Moshi for an adapter of the element type {@code X} and then * uses that to create an adapter for the set. */ static class SortedSetAdapterFactory implements JsonAdapter.Factory { @Override public @Nullable JsonAdapter create( Type type, Set annotations, Moshi moshi) { if (!annotations.isEmpty()) { return null; // Annotations? This factory doesn't apply. } if (!(type instanceof ParameterizedType)) { return null; // No type parameter? This factory doesn't apply. } ParameterizedType parameterizedType = (ParameterizedType) type; if (parameterizedType.getRawType() != SortedSet.class) { return null; // Not a sorted set? This factory doesn't apply. } Type elementType = parameterizedType.getActualTypeArguments()[0]; JsonAdapter elementAdapter = moshi.adapter(elementType); return new SortedSetAdapter<>(elementAdapter).nullSafe(); } } public static void main(String[] args) throws Exception { new CustomAdapterFactory().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/CustomAdapterWithDelegate.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.FromJson; import com.squareup.moshi.Json; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonReader; import com.squareup.moshi.Moshi; import java.io.IOException; import javax.annotation.Nullable; public final class CustomAdapterWithDelegate { public void run() throws Exception { // We want to match any Stage that starts with 'in-progress' as Stage.IN_PROGRESS // and leave the rest of the enum values as to match as normal. Moshi moshi = new Moshi.Builder().add(new StageAdapter()).build(); JsonAdapter jsonAdapter = moshi.adapter(Stage.class); System.out.println(jsonAdapter.fromJson("\"not-started\"")); System.out.println(jsonAdapter.fromJson("\"in-progress\"")); System.out.println(jsonAdapter.fromJson("\"in-progress-step1\"")); } public static void main(String[] args) throws Exception { new CustomAdapterWithDelegate().run(); } private enum Stage { @Json(name = "not-started") NOT_STARTED, @Json(name = "in-progress") IN_PROGRESS, @Json(name = "rejected") REJECTED, @Json(name = "completed") COMPLETED } private static final class StageAdapter { @FromJson @Nullable Stage fromJson(JsonReader jsonReader, JsonAdapter delegate) throws IOException { String value = jsonReader.nextString(); Stage stage; if (value.startsWith("in-progress")) { stage = Stage.IN_PROGRESS; } else { stage = delegate.fromJsonValue(value); } return stage; } } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/CustomFieldName.java ================================================ /* * Copyright (C) 2016 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.recipes.models.Player; public final class CustomFieldName { public void run() throws Exception { String json = "" + "{" + " \"username\": \"jesse\"," + " \"lucky number\": 32" + "}\n"; Moshi moshi = new Moshi.Builder().build(); JsonAdapter jsonAdapter = moshi.adapter(Player.class); Player player = jsonAdapter.fromJson(json); System.out.println(player); } public static void main(String[] args) throws Exception { new CustomFieldName().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/CustomQualifier.java ================================================ /* * Copyright (C) 2016 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 * * https://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.squareup.moshi.recipes; import static java.lang.annotation.RetentionPolicy.RUNTIME; 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.lang.annotation.Retention; public final class CustomQualifier { public void run() throws Exception { String json = "" + "{\n" + " \"color\": \"#ff0000\",\n" + " \"height\": 768,\n" + " \"width\": 1024\n" + "}\n"; Moshi moshi = new Moshi.Builder().add(new ColorAdapter()).build(); JsonAdapter jsonAdapter = moshi.adapter(Rectangle.class); Rectangle rectangle = jsonAdapter.fromJson(json); System.out.println(rectangle); } public static void main(String[] args) throws Exception { new CustomQualifier().run(); } static class Rectangle { int width; int height; @HexColor int color; @Override public String toString() { return String.format("%dx%d #%06x", width, height, color); } } @Retention(RUNTIME) @JsonQualifier public @interface HexColor {} static class ColorAdapter { @ToJson String toJson(@HexColor int rgb) { return String.format("#%06x", rgb); } @FromJson @HexColor int fromJson(String rgb) { return Integer.parseInt(rgb.substring(1), 16); } } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/CustomTypeAdapter.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.recipes.models.BlackjackHand; public final class CustomTypeAdapter { public void run() throws Exception { String json = "" + "{\n" + " \"hidden_card\": \"6S\",\n" + " \"visible_cards\": [\n" + " \"4C\",\n" + " \"AH\"\n" + " ]\n" + "}\n"; Moshi moshi = new Moshi.Builder().add(new CardAdapter()).build(); JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class); BlackjackHand blackjackHand = jsonAdapter.fromJson(json); System.out.println(blackjackHand); } public static void main(String[] args) throws Exception { new CustomTypeAdapter().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/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 * * https://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.squareup.moshi.recipes; 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 java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Set; import javax.annotation.Nullable; public final class DefaultOnDataMismatchAdapter extends JsonAdapter { private final JsonAdapter delegate; private final T defaultValue; private DefaultOnDataMismatchAdapter(JsonAdapter delegate, T defaultValue) { this.delegate = delegate; this.defaultValue = defaultValue; } @Override public T fromJson(JsonReader reader) throws IOException { // Use a peeked reader to leave the reader in a known state even if there's an exception. JsonReader peeked = reader.peekJson(); T result; try { // Attempt to decode to the target type with the peeked reader. result = delegate.fromJson(peeked); } catch (JsonDataException e) { result = defaultValue; } finally { peeked.close(); } // Skip the value back on the reader, no matter the state of the peeked reader. reader.skipValue(); return result; } @Override public void toJson(JsonWriter writer, T value) throws IOException { delegate.toJson(writer, value); } public static Factory newFactory(final Class type, final T defaultValue) { return new Factory() { @Override public @Nullable JsonAdapter create( Type requestedType, Set annotations, Moshi moshi) { if (type != requestedType) return null; JsonAdapter delegate = moshi.nextAdapter(this, type, annotations); return new DefaultOnDataMismatchAdapter<>(delegate, defaultValue); } }; } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/FallbackEnum.java ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.recipes; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import com.squareup.moshi.internal.Util; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.reflect.Type; import java.util.Set; import javax.annotation.Nullable; final class FallbackEnum { @Retention(RUNTIME) @JsonQualifier public @interface Fallback { /** The enum name. */ String value(); } public static final class FallbackEnumJsonAdapter> extends JsonAdapter { public static final Factory FACTORY = new Factory() { @Nullable @Override @SuppressWarnings("unchecked") public JsonAdapter create( Type type, Set annotations, Moshi moshi) { Class rawType = Types.getRawType(type); if (!rawType.isEnum()) { return null; } if (annotations.size() != 1) { return null; } Annotation annotation = annotations.iterator().next(); if (!(annotation instanceof Fallback)) { return null; } //noinspection rawtypes return new FallbackEnumJsonAdapter<>( (Class) rawType, ((Fallback) annotation).value()); } }; final Class enumType; final String[] nameStrings; final T[] constants; final JsonReader.Options options; final T defaultValue; FallbackEnumJsonAdapter(Class enumType, String fallbackName) { this.enumType = enumType; this.defaultValue = Enum.valueOf(enumType, fallbackName); try { constants = enumType.getEnumConstants(); nameStrings = new String[constants.length]; for (int i = 0; i < constants.length; i++) { String constantName = constants[i].name(); nameStrings[i] = Util.jsonName(enumType.getField(constantName), constantName); } 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 defaultValue; } @Override public void toJson(JsonWriter writer, T value) throws IOException { writer.value(nameStrings[value.ordinal()]); } @Override public String toString() { return "JsonAdapter(" + enumType.getName() + ").defaultValue( " + defaultValue + ")"; } } static final class Example { enum Transportation { WALKING, BIKING, TRAINS, PLANES } @Fallback("WALKING") final Transportation transportation; Example(Transportation transportation) { this.transportation = transportation; } @Override public String toString() { return transportation.toString(); } } public static void main(String[] args) throws Exception { Moshi moshi = new Moshi.Builder().add(FallbackEnumJsonAdapter.FACTORY).build(); JsonAdapter adapter = moshi.adapter(Example.class); System.out.println(adapter.fromJson("{\"transportation\":\"CARS\"}")); } private FallbackEnum() {} } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/FromJsonWithoutStrings.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.FromJson; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.ToJson; public final class FromJsonWithoutStrings { public void run() throws Exception { // For some reason our JSON has date and time as separate fields. We will clean that up during // parsing: Moshi will first parse the JSON directly to an EventJson and from that the // EventJsonAdapter will create the actual Event. String json = "" + "{\n" + " \"title\": \"Blackjack tournament\",\n" + " \"begin_date\": \"20151010\",\n" + " \"begin_time\": \"17:04\"\n" + "}\n"; Moshi moshi = new Moshi.Builder().add(new EventJsonAdapter()).build(); JsonAdapter jsonAdapter = moshi.adapter(Event.class); Event event = jsonAdapter.fromJson(json); System.out.println(event); System.out.println(jsonAdapter.toJson(event)); } public static void main(String[] args) throws Exception { new FromJsonWithoutStrings().run(); } private static final class EventJson { String title; String begin_date; String begin_time; } public static final class Event { String title; String beginDateAndTime; @Override public String toString() { return "Event{" + "title='" + title + '\'' + ", beginDateAndTime='" + beginDateAndTime + '\'' + '}'; } } private static final class EventJsonAdapter { @FromJson Event eventFromJson(EventJson eventJson) { Event event = new Event(); event.title = eventJson.title; event.beginDateAndTime = eventJson.begin_date + " " + eventJson.begin_time; return event; } @ToJson EventJson eventToJson(Event event) { EventJson json = new EventJson(); json.title = event.title; json.begin_date = event.beginDateAndTime.substring(0, 8); json.begin_time = event.beginDateAndTime.substring(9, 14); return json; } } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/IncludeNullsForAnnotatedTypes.java ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi.recipes; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.Set; public final class IncludeNullsForAnnotatedTypes { public void run() throws Exception { Moshi moshi = new Moshi.Builder().add(new AlwaysSerializeNullsFactory()).build(); JsonAdapter driverAdapter = moshi.adapter(Driver.class); Car car = new Car(); car.make = "Ford"; car.model = "Mach-E"; car.color = null; // This null will show up in the JSON because Car has @AlwaysSerializeNulls. Driver driver = new Driver(); driver.name = "Jesse"; driver.emailAddress = null; // This null will be omitted. driver.favoriteCar = car; System.out.println(driverAdapter.toJson(driver)); } @Target(TYPE) @Retention(RUNTIME) public @interface AlwaysSerializeNulls {} @AlwaysSerializeNulls static class Car { String make; String model; String color; } static class Driver { String name; String emailAddress; Car favoriteCar; } static class AlwaysSerializeNullsFactory implements JsonAdapter.Factory { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { Class rawType = Types.getRawType(type); if (!rawType.isAnnotationPresent(AlwaysSerializeNulls.class)) { return null; } JsonAdapter delegate = moshi.nextAdapter(this, type, annotations); return delegate.serializeNulls(); } } public static void main(String[] args) throws Exception { new IncludeNullsForAnnotatedTypes().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/IncludeNullsForOneType.java ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import com.squareup.moshi.ToJson; import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter; import com.squareup.moshi.recipes.models.Tournament; import java.io.IOException; import java.util.Date; public final class IncludeNullsForOneType { public void run() throws Exception { Moshi moshi = new Moshi.Builder() .add(Date.class, new Rfc3339DateJsonAdapter()) .add(new TournamentWithNullsAdapter()) .build(); JsonAdapter tournamentAdapter = moshi.adapter(Tournament.class); // Moshi normally skips nulls, but with our adapter registered they are emitted. Tournament withNulls = new Tournament("Waterloo Classic", null, null); System.out.println(tournamentAdapter.toJson(withNulls)); } public static final class TournamentWithNullsAdapter { @ToJson void toJson(JsonWriter writer, Tournament tournament, JsonAdapter delegate) throws IOException { boolean wasSerializeNulls = writer.getSerializeNulls(); writer.setSerializeNulls(true); try { // Once we've customized the JSON writer, we let the default JSON adapter do its job. delegate.toJson(writer, tournament); } finally { writer.setLenient(wasSerializeNulls); } } } public static void main(String[] args) throws Exception { new IncludeNullsForOneType().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/JsonString.kt ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi.recipes import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonQualifier import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi.Builder import com.squareup.moshi.Types import java.lang.reflect.Type import kotlin.annotation.AnnotationRetention.RUNTIME import okio.BufferedSource @JsonClass(generateAdapter = true) data class ExampleClass(val type: Int, @JsonString val rawJson: String) @Retention(RUNTIME) @JsonQualifier annotation class JsonString class JsonStringJsonAdapterFactory : JsonAdapter.Factory { override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (type != String::class.java) return null Types.nextAnnotations(annotations, JsonString::class.java) ?: return null return JsonStringJsonAdapter().nullSafe() } private class JsonStringJsonAdapter : JsonAdapter() { override fun fromJson(reader: JsonReader): String = reader.nextSource().use(BufferedSource::readUtf8) override fun toJson(writer: JsonWriter, value: String) { writer.valueSink().use { sink -> sink.writeUtf8(value) } } } } fun main() { // language=JSON val json = "{\"type\":1,\"rawJson\":{\"a\":2,\"b\":3,\"c\":[1,2,3]}}" val moshi = Builder().add(JsonStringJsonAdapterFactory()).build() val example: ExampleClass = moshi.adapter().fromJson(json) check(example.type == 1) // language=JSON check(example.rawJson == "{\"a\":2,\"b\":3,\"c\":[1,2,3]}") } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/MultipleFormats.java ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.FromJson; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import com.squareup.moshi.ToJson; import com.squareup.moshi.recipes.models.Card; import com.squareup.moshi.recipes.models.Suit; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; public final class MultipleFormats { public void run() throws Exception { Moshi moshi = new Moshi.Builder() .add(new MultipleFormatsCardAdapter()) .add(new CardStringAdapter()) .build(); JsonAdapter cardAdapter = moshi.adapter(Card.class); // Decode cards from one format or the other. System.out.println(cardAdapter.fromJson("\"5D\"")); System.out.println(cardAdapter.fromJson("{\"suit\": \"SPADES\", \"rank\": 5}")); // Cards are always encoded as strings. System.out.println(cardAdapter.toJson(new Card('5', Suit.CLUBS))); } /** Handles cards either as strings "5D" or as objects {"suit": "SPADES", "rank": 5}. */ public final class MultipleFormatsCardAdapter { @ToJson void toJson(JsonWriter writer, Card value, @CardString JsonAdapter stringAdapter) throws IOException { stringAdapter.toJson(writer, value); } @FromJson Card fromJson( JsonReader reader, @CardString JsonAdapter stringAdapter, JsonAdapter defaultAdapter) throws IOException { if (reader.peek() == JsonReader.Token.STRING) { return stringAdapter.fromJson(reader); } else { return defaultAdapter.fromJson(reader); } } } /** Handles cards as strings only. */ public final class CardStringAdapter { @ToJson String toJson(@CardString Card card) { return card.rank + card.suit.name().substring(0, 1); } @FromJson @CardString Card fromJson(String card) { if (card.length() != 2) throw new JsonDataException("Unknown card: " + card); char rank = card.charAt(0); switch (card.charAt(1)) { case 'C': return new Card(rank, Suit.CLUBS); case 'D': return new Card(rank, Suit.DIAMONDS); case 'H': return new Card(rank, Suit.HEARTS); case 'S': return new Card(rank, Suit.SPADES); default: throw new JsonDataException("unknown suit: " + card); } } } @Retention(RetentionPolicy.RUNTIME) @JsonQualifier @interface CardString {} public static void main(String[] args) throws Exception { new MultipleFormats().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/ReadAndWriteRfc3339Dates.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter; import com.squareup.moshi.recipes.models.Tournament; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; import java.util.concurrent.TimeUnit; public final class ReadAndWriteRfc3339Dates { public void run() throws Exception { Moshi moshi = new Moshi.Builder().add(Date.class, new Rfc3339DateJsonAdapter()).build(); JsonAdapter jsonAdapter = moshi.adapter(Tournament.class); // The RFC3339 JSON adapter can read dates with a timezone offset like '-05:00'. String lastTournament = "" + "{" + " \"location\":\"Chainsaw\"," + " \"name\":\"21 for 21\"," + " \"start\":\"2015-09-01T20:00:00-05:00\"" + "}"; System.out.println("Last tournament: " + jsonAdapter.fromJson(lastTournament)); // The RFC3339 JSON adapter always writes dates with UTC, using a 'Z' suffix. Tournament nextTournament = new Tournament("Waterloo Classic", "Bauer Kitchen", newDate(2015, 10, 1, 20, -5)); System.out.println("Next tournament JSON: " + jsonAdapter.toJson(nextTournament)); } public static void main(String[] args) throws Exception { new ReadAndWriteRfc3339Dates().run(); } private Date newDate(int year, int month, int day, int hour, int offset) { Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT")); calendar.set(year, month - 1, day, hour, 0, 0); calendar.set(Calendar.MILLISECOND, 0); return new Date(calendar.getTimeInMillis() - TimeUnit.HOURS.toMillis(offset)); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/ReadJson.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.recipes.models.BlackjackHand; public final class ReadJson { public void run() throws Exception { String json = "" + "{\n" + " \"hidden_card\": {\n" + " \"rank\": \"6\",\n" + " \"suit\": \"SPADES\"\n" + " },\n" + " \"visible_cards\": [\n" + " {\n" + " \"rank\": \"4\",\n" + " \"suit\": \"CLUBS\"\n" + " },\n" + " {\n" + " \"rank\": \"A\",\n" + " \"suit\": \"HEARTS\"\n" + " }\n" + " ]\n" + "}\n"; Moshi moshi = new Moshi.Builder().build(); JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class); BlackjackHand blackjackHand = jsonAdapter.fromJson(json); System.out.println(blackjackHand); } public static void main(String[] args) throws Exception { new ReadJson().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/ReadJsonList.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import com.squareup.moshi.recipes.models.Card; import java.lang.reflect.Type; import java.util.List; public final class ReadJsonList { public void run() throws Exception { String json = "" + "[\n" + " {\n" + " \"rank\": \"4\",\n" + " \"suit\": \"CLUBS\"\n" + " },\n" + " {\n" + " \"rank\": \"A\",\n" + " \"suit\": \"HEARTS\"\n" + " },\n" + " {\n" + " \"rank\": \"J\",\n" + " \"suit\": \"SPADES\"\n" + " }\n" + "]"; Moshi moshi = new Moshi.Builder().build(); Type listOfCardsType = Types.newParameterizedType(List.class, Card.class); JsonAdapter> jsonAdapter = moshi.adapter(listOfCardsType); List cards = jsonAdapter.fromJson(json); System.out.println(cards); } public static void main(String[] args) throws Exception { new ReadJsonList().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/ReadJsonListKt.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import com.squareup.moshi.recipes.models.Card class ReadJsonListKt { // language=JSON private val jsonString = """ [{"rank": "4", "suit": "CLUBS" }, {"rank": "A", "suit": "HEARTS" }, {"rank": "J", "suit": "SPADES" }] """ .trimIndent() fun readJsonList() { val jsonAdapter = Moshi.Builder().build().adapter>() val cards = jsonAdapter.fromJson(jsonString) println(cards) cards[0].run { println(rank) println(suit) } } } fun main() { ReadJsonListKt().readJsonList() } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/RecoverFromTypeMismatch.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 * * https://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.squareup.moshi.recipes; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import com.squareup.moshi.recipes.models.Suit; import java.util.List; public final class RecoverFromTypeMismatch { public void run() throws Exception { String json = "[\"DIAMONDS\", \"STARS\", \"HEARTS\"]"; Moshi moshi = new Moshi.Builder() .add(DefaultOnDataMismatchAdapter.newFactory(Suit.class, Suit.CLUBS)) .build(); JsonAdapter> jsonAdapter = moshi.adapter(Types.newParameterizedType(List.class, Suit.class)); List suits = jsonAdapter.fromJson(json); System.out.println(suits); } public static void main(String[] args) throws Exception { new RecoverFromTypeMismatch().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/Unwrap.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 * * https://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.squareup.moshi.recipes; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import com.squareup.moshi.Types; import com.squareup.moshi.recipes.Unwrap.EnvelopeJsonAdapter.Enveloped; import com.squareup.moshi.recipes.models.Card; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.reflect.Type; import java.util.Set; import javax.annotation.Nullable; final class Unwrap { private Unwrap() {} public static void main(String[] args) throws Exception { String json = "" + "{\"data\":" + " {\n" + " \"rank\": \"4\",\n" + " \"suit\": \"CLUBS\"\n" + " }" + "}"; Moshi moshi = new Moshi.Builder().add(EnvelopeJsonAdapter.FACTORY).build(); JsonAdapter adapter = moshi.adapter(Card.class, Enveloped.class); Card out = adapter.fromJson(json); System.out.println(out); } public static final class EnvelopeJsonAdapter extends JsonAdapter { public static final JsonAdapter.Factory FACTORY = new Factory() { @Override public @Nullable JsonAdapter create( Type type, Set annotations, Moshi moshi) { Set delegateAnnotations = Types.nextAnnotations(annotations, Enveloped.class); if (delegateAnnotations == null) { return null; } Type envelope = Types.newParameterizedTypeWithOwner( EnvelopeJsonAdapter.class, Envelope.class, type); JsonAdapter> delegate = moshi.nextAdapter(this, envelope, delegateAnnotations); return new EnvelopeJsonAdapter(delegate); } }; @Retention(RUNTIME) @JsonQualifier public @interface Enveloped {} private static final class Envelope { final T data; Envelope(T data) { this.data = data; } } private final JsonAdapter> delegate; EnvelopeJsonAdapter(JsonAdapter> delegate) { this.delegate = delegate; } @Override public Object fromJson(JsonReader reader) throws IOException { return delegate.fromJson(reader).data; } @Override public void toJson(JsonWriter writer, Object value) throws IOException { delegate.toJson(writer, new Envelope<>(value)); } } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/WriteJson.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes; import static com.squareup.moshi.recipes.models.Suit.CLUBS; import static com.squareup.moshi.recipes.models.Suit.HEARTS; import static com.squareup.moshi.recipes.models.Suit.SPADES; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.Moshi; import com.squareup.moshi.recipes.models.BlackjackHand; import com.squareup.moshi.recipes.models.Card; import java.util.Arrays; public final class WriteJson { public void run() throws Exception { BlackjackHand blackjackHand = new BlackjackHand( new Card('6', SPADES), Arrays.asList(new Card('4', CLUBS), new Card('A', HEARTS))); Moshi moshi = new Moshi.Builder().build(); JsonAdapter jsonAdapter = moshi.adapter(BlackjackHand.class); String json = jsonAdapter.toJson(blackjackHand); System.out.println(json); } public static void main(String[] args) throws Exception { new WriteJson().run(); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/models/BlackjackHand.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes.models; import java.util.List; public final class BlackjackHand { public final Card hidden_card; public final List visible_cards; public BlackjackHand(Card hiddenCard, List visibleCards) { this.hidden_card = hiddenCard; this.visible_cards = visibleCards; } @Override public String toString() { return "hidden=" + hidden_card + ",visible=" + visible_cards; } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/models/Card.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes.models; public final class Card { public final char rank; public final Suit suit; public Card(char rank, Suit suit) { this.rank = rank; this.suit = suit; } @Override public String toString() { return String.format("%s%s", rank, suit); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/models/Player.java ================================================ /* * Copyright (C) 2016 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 * * https://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.squareup.moshi.recipes.models; import com.squareup.moshi.Json; public final class Player { public final String username; public final @Json(name = "lucky number") int luckyNumber; public Player(String username, int luckyNumber) { this.username = username; this.luckyNumber = luckyNumber; } @Override public String toString() { return username + " gets lucky with " + luckyNumber; } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/models/Suit.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes.models; public enum Suit { CLUBS, DIAMONDS, HEARTS, SPADES; @Override public String toString() { return name().substring(0, 1); } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/models/Tournament.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.recipes.models; import java.util.Date; public final class Tournament { public final String name; public final String location; public final Date start; public Tournament(String name, String location, Date start) { this.name = name; this.location = location; this.start = start; } @Override public String toString() { return name + " at " + location + " on " + start; } } ================================================ FILE: examples/src/main/java/com/squareup/moshi/recipes/package-info.java ================================================ /** Moshi code samples. */ @javax.annotation.ParametersAreNonnullByDefault package com.squareup.moshi.recipes; ================================================ FILE: gradle/libs.versions.toml ================================================ [versions] autoService = "1.1.1" jdk = "21" jvmTarget = "1.8" kotlin = "2.3.20" kotlinCompileTesting = "0.12.1" kotlinpoet = "2.2.0" ksp = "2.3.6" [plugins] dokka = { id = "org.jetbrains.dokka", version = "2.1.0" } japicmp = { id = "me.champeau.gradle.japicmp", version = "0.4.6" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.36.0" } spotless = { id = "com.diffplug.spotless", version = "8.4.0" } [libraries] asm = "org.ow2.asm:asm:9.9.1" autoCommon = "com.google.auto:auto-common:1.2.2" autoService = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoService" } autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.2.0" jsr305 = "com.google.code.findbugs:jsr305:3.0.2" kotlin-annotationProcessingEmbeddable = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable", version.ref = "kotlin" } kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } kotlin-metadata = { module = "org.jetbrains.kotlin:kotlin-metadata-jvm", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet" } ksp = { module = "com.google.devtools.ksp:symbol-processing", version.ref = "ksp" } ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } okio = "com.squareup.okio:okio:3.17.0" assertj = "org.assertj:assertj-core:3.27.7" assertk = "com.willowtreeapps.assertk:assertk:0.28.1" junit = "junit:junit:4.13.2" kotlinCompileTesting = { module = "dev.zacsweers.kctfork:core", version.ref = "kotlinCompileTesting" } kotlinCompileTesting-ksp = { module = "dev.zacsweers.kctfork:ksp", version.ref ="kotlinCompileTesting" } truth = "com.google.truth:truth:1.4.5" googleJavaFormat = "com.google.googlejavaformat:google-java-format:1.35.0" ktfmt = "com.facebook:ktfmt:0.62" ================================================ FILE: gradle/wrapper/gradle-wrapper.properties ================================================ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists ================================================ FILE: gradle.properties ================================================ # Memory for Dokka https://github.com/Kotlin/dokka/issues/1405 org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 VERSION_NAME=2.0.0-SNAPSHOT ================================================ FILE: gradlew ================================================ #!/bin/sh # # Copyright © 2015 the original authors. # # 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 # # https://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. # # SPDX-License-Identifier: Apache-2.0 # ############################################################################## # # Gradle start up script for POSIX generated by Gradle. # # Important for running: # # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is # noncompliant, but you have some other compliant shell such as ksh or # bash, then to run this script, type that shell name before the whole # command line, like: # # ksh Gradle # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», # «${var#prefix}», «${var%suffix}», and «$( cmd )»; # * compound commands having a testable exit status, especially «case»; # * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # # (2) This script targets any POSIX shell, so it avoids extensions provided # by Bash, Ksh, etc; in particular arrays are avoided. # # The "traditional" practice of packing multiple parameters into a # space-separated string is a well documented source of bugs and security # problems, so this is (mostly) avoided, by progressively accumulating # options in "$@", and eventually passing that to Java. # # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; # see the in-line comments for details. # # There are tweaks for specific operating systems such as AIX, CygWin, # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template # https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. # ############################################################################## # Attempt to set APP_HOME # Resolve links: $0 may be a link app_path=$0 # Need this for daisy-chained symlinks. while APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path [ -h "$app_path" ] do ls=$( ls -ld "$app_path" ) link=${ls#*' -> '} case $link in #( /*) app_path=$link ;; #( *) app_path=$APP_HOME$link ;; esac done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum warn () { echo "$*" } >&2 die () { echo echo "$*" echo exit 1 } >&2 # 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 ;; #( MSYS* | MINGW* ) msys=true ;; #( NONSTOP* ) nonstop=true ;; esac # 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 if ! command -v java >/dev/null 2>&1 then 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 fi # Increase the maximum file descriptors if we can. if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac case $MAX_FD in #( '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi # Collect all arguments for the java command, stacking in reverse order: # * args from the command line # * the main class name # * -classpath # * -D...appname settings # * --module-path (only if needed) # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) # Now convert the arguments - kludge to limit ourselves to /bin/sh for arg do if case $arg in #( -*) false ;; # don't mess with options #( /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath [ -e "$t" ] ;; #( *) false ;; esac then arg=$( cygpath --path --ignore --mixed "$arg" ) fi # Roll the args list around exactly as many times as the number of # args, so each arg winds up back in the position where it started, but # possibly modified. # # NB: a `for` loop captures its iteration list before it begins, so # changing the positional parameters here affects neither the number of # iterations, nor the values presented in `arg`. shift # remove old arg set -- "$@" "$arg" # push replacement arg done fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. if ! command -v xargs >/dev/null 2>&1 then die "xargs is not available" fi # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. # # In Bash we could simply go: # # readarray ARGS < <( xargs -n1 <<<"$var" ) && # set -- "${ARGS[@]}" "$@" # # but POSIX shell has neither arrays nor command substitution, so instead we # post-process each arg (as a line of input to sed) to backslash-escape any # character that might be a shell metacharacter, then use eval to reverse # that process (while maintaining the separation between arguments), and wrap # the whole thing up as a single "set" statement. # # This will of course break if any of these variables contains a newline or # an unmatched quote. # eval "set -- $( printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | xargs -n1 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | tr '\n' ' ' )" '"$@"' exec "$JAVACMD" "$@" ================================================ FILE: gradlew.bat ================================================ @rem @rem Copyright 2015 the original author or authors. @rem @rem Licensed under the Apache License, Version 2.0 (the "License"); @rem you may not use this file except in compliance with the License. @rem You may obtain a copy of the License at @rem @rem https://www.apache.org/licenses/LICENSE-2.0 @rem @rem Unless required by applicable law or agreed to in writing, software @rem distributed under the License is distributed on an "AS IS" BASIS, @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem @rem SPDX-License-Identifier: Apache-2.0 @rem @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=. @rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Resolve any "." and ".." in APP_HOME to make it shorter. for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi @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="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute echo. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute echo. 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 goto fail :execute @rem Setup the command line @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell if %ERRORLEVEL% equ 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! set EXIT_CODE=%ERRORLEVEL% if %EXIT_CODE% equ 0 set EXIT_CODE=1 if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal :omega ================================================ FILE: moshi/build.gradle.kts ================================================ import org.gradle.jvm.tasks.Jar import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") id("com.vanniktech.maven.publish") id("org.jetbrains.dokka") } kotlin.target { val main = compilations.getByName(MAIN_COMPILATION_NAME) val java16 = compilations.create("java16") { associateWith(main) defaultSourceSet.kotlin.srcDir("src/main/java16") compileJavaTaskProvider.configure { options.release = 16 } compileTaskProvider.configure { (compilerOptions as KotlinJvmCompilerOptions).jvmTarget = JvmTarget.JVM_16 } } // Package our actual RecordJsonAdapter from java16 sources in and denote it as an MRJAR tasks.named(artifactsTaskName) { from(java16.output) { into("META-INF/versions/16") exclude("META-INF") } manifest { attributes("Multi-Release" to "true") } } } tasks.withType().configureEach { // ExtendsPlatformClassWithProtectedField tests a case where we set a protected // ByteArrayOutputStream.buf field jvmArgs("--add-opens=java.base/java.io=ALL-UNNAMED") } tasks.withType().configureEach { compilerOptions { freeCompilerArgs.addAll("-opt-in=kotlin.contracts.ExperimentalContracts", "-Xjvm-default=all") if (name.contains("test", true)) { freeCompilerArgs.add("-opt-in=kotlin.ExperimentalStdlibApi") } } } dependencies { compileOnly(libs.jsr305) api(libs.okio) testCompileOnly(libs.jsr305) testImplementation(libs.assertk) testImplementation(libs.junit) testImplementation(libs.kotlin.reflect) testImplementation(libs.truth) } tasks.withType().configureEach { manifest { attributes("Automatic-Module-Name" to "com.squareup.moshi") } } ================================================ FILE: moshi/gradle.properties ================================================ kotlin.build.archivesTaskOutputAsFriendModule=false ================================================ FILE: moshi/japicmp/build.gradle.kts ================================================ import me.champeau.gradle.japicmp.JapicmpTask plugins { `java-library` id("me.champeau.gradle.japicmp") } val baseline = configurations.create("baseline") val latest = configurations.create("latest") dependencies { baseline("com.squareup.moshi:moshi:1.15.2") { isTransitive = false version { strictly("1.14.0") } } latest(project(":moshi")) } val japicmp = tasks.register("japicmp") { dependsOn("jar") oldClasspath.from(baseline) newClasspath.from(latest) onlyBinaryIncompatibleModified.set(true) failOnModification.set(true) txtOutputFile.set(layout.buildDirectory.file("reports/japi.txt")) ignoreMissingClasses.set(true) includeSynthetic.set(true) classExcludes.addAll( // Internal. "com.squareup.moshi.AdapterMethodsFactory", "com.squareup.moshi.ClassJsonAdapter", "com.squareup.moshi.internal.NonNullJsonAdapter", "com.squareup.moshi.internal.NullSafeJsonAdapter", "com.squareup.moshi.internal.Util\$GenericArrayTypeImpl", "com.squareup.moshi.internal.Util\$ParameterizedTypeImpl", "com.squareup.moshi.internal.Util\$WildcardTypeImpl", // Package-private "com.squareup.moshi.RecordJsonAdapter\$ComponentBinding", "com.squareup.moshi.StandardJsonAdapters", ) methodExcludes.addAll( // Was unintentionally open before "com.squareup.moshi.JsonAdapter#indent(java.lang.String)", "com.squareup.moshi.internal.Util#hasNullable(java.lang.annotation.Annotation[])", "com.squareup.moshi.internal.Util#jsonAnnotations(java.lang.annotation.Annotation[])", "com.squareup.moshi.internal.Util#jsonAnnotations(java.lang.reflect.AnnotatedElement)", "com.squareup.moshi.internal.Util#jsonName(java.lang.String, com.squareup.moshi.Json)", "com.squareup.moshi.internal.Util#jsonName(java.lang.String, java.lang.reflect.AnnotatedElement)", "com.squareup.moshi.internal.Util#resolve(java.lang.reflect.Type, java.lang.Class, java.lang.reflect.Type)", "com.squareup.moshi.internal.Util#typeAnnotatedWithAnnotations(java.lang.reflect.Type, java.util.Set)", "com.squareup.moshi.internal.Util#typesMatch(java.lang.reflect.Type, java.lang.reflect.Type)", ) fieldExcludes.addAll( // False-positive, class is not public anyway "com.squareup.moshi.CollectionJsonAdapter#FACTORY", // Class is not public "com.squareup.moshi.MapJsonAdapter#FACTORY", // Class is not public "com.squareup.moshi.ArrayJsonAdapter#FACTORY", ) } tasks.named("check").configure { dependsOn(japicmp) } ================================================ FILE: moshi/records-tests/build.gradle.kts ================================================ plugins { `java-library` } java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) } } tasks.withType().configureEach { // Target 16 to ensure minimum compatibility options.release.set(16) } dependencies { testImplementation(project(":moshi")) testCompileOnly(libs.jsr305) testImplementation(libs.junit) testImplementation(libs.truth) } ================================================ FILE: moshi/records-tests/src/test/java/com/squareup/moshi/records/RecordsTest.java ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.records; import static com.google.common.truth.Truth.assertThat; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.Assert.fail; import com.squareup.moshi.FromJson; import com.squareup.moshi.Json; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.JsonQualifier; import com.squareup.moshi.JsonReader; import com.squareup.moshi.JsonWriter; import com.squareup.moshi.Moshi; import com.squareup.moshi.ToJson; import com.squareup.moshi.Types; import java.io.IOException; import java.lang.annotation.Retention; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import okio.Buffer; import org.junit.Test; public final class RecordsTest { private final Moshi moshi = new Moshi.Builder().build(); @Test public void smokeTest() throws IOException { var stringAdapter = moshi.adapter(String.class); var adapter = moshi .newBuilder() .add(CharSequence.class, stringAdapter) .add(Types.subtypeOf(CharSequence.class), stringAdapter) .add(Types.supertypeOf(CharSequence.class), stringAdapter) .build() .adapter(SmokeTestType.class); var instance = new SmokeTestType( "John", "Smith", 25, List.of("American"), 70.5f, null, true, List.of("super wildcards!"), List.of("extend wildcards!"), List.of("unbounded"), List.of("objectList"), new int[] {1, 2, 3}, new String[] {"fav", "arrays"}, Map.of("italian", "pasta"), Set.of(List.of(Map.of("someKey", new int[] {1}))), new Map[] {Map.of("Hello", "value")}); var json = adapter.toJson(instance); var deserialized = adapter.fromJson(json); assertThat(deserialized).isEqualTo(instance); } public record SmokeTestType( @Json(name = "first_name") String firstName, @Json(name = "last_name") String lastName, int age, List nationalities, float weight, Boolean tattoos, // Boxed primitive test boolean hasChildren, List superWildcard, List extendsWildcard, List unboundedWildcard, List objectList, int[] favoriteThreeNumbers, String[] favoriteArrayValues, Map foodPreferences, Set>> setListMapArrayInt, Map[] nestedArray) { @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; SmokeTestType that = (SmokeTestType) o; return age == that.age && Float.compare(that.weight, weight) == 0 && hasChildren == that.hasChildren && firstName.equals(that.firstName) && lastName.equals(that.lastName) && nationalities.equals(that.nationalities) && Objects.equals(tattoos, that.tattoos) && superWildcard.equals(that.superWildcard) && extendsWildcard.equals(that.extendsWildcard) && unboundedWildcard.equals(that.unboundedWildcard) && objectList.equals(that.objectList) && Arrays.equals(favoriteThreeNumbers, that.favoriteThreeNumbers) && Arrays.equals(favoriteArrayValues, that.favoriteArrayValues) && foodPreferences.equals(that.foodPreferences) // && setListMapArrayInt.equals(that.setListMapArrayInt) // Nested array equality doesn't // carry over && Arrays.equals(nestedArray, that.nestedArray); } @Override public int hashCode() { int result = Objects.hash( firstName, lastName, age, nationalities, weight, tattoos, hasChildren, superWildcard, extendsWildcard, unboundedWildcard, objectList, foodPreferences, setListMapArrayInt); result = 31 * result + Arrays.hashCode(favoriteThreeNumbers); result = 31 * result + Arrays.hashCode(favoriteArrayValues); result = 31 * result + Arrays.hashCode(nestedArray); return result; } } @Test public void genericRecord() throws IOException { var adapter = moshi.>adapter( Types.newParameterizedTypeWithOwner( RecordsTest.class, GenericRecord.class, String.class)); assertThat(adapter.fromJson("{\"value\":\"Okay!\"}")).isEqualTo(new GenericRecord<>("Okay!")); } public record GenericRecord(T value) {} @Test public void genericBoundedRecord() throws IOException { var adapter = moshi.>adapter( Types.newParameterizedTypeWithOwner( RecordsTest.class, GenericBoundedRecord.class, Integer.class)); assertThat(adapter.fromJson("{\"value\":4}")).isEqualTo(new GenericBoundedRecord<>(4)); } @Test public void indirectGenerics() throws IOException { var value = new HasIndirectGenerics( new IndirectGenerics<>(1L, List.of(2L, 3L, 4L), Map.of("five", 5L))); var jsonAdapter = moshi.adapter(HasIndirectGenerics.class); var json = "{\"value\":{\"single\":1,\"list\":[2,3,4],\"map\":{\"five\":5}}}"; assertThat(jsonAdapter.toJson(value)).isEqualTo(json); assertThat(jsonAdapter.fromJson(json)).isEqualTo(value); } public record IndirectGenerics(T single, List list, Map map) {} public record HasIndirectGenerics(IndirectGenerics value) {} @Test public void qualifiedValues() throws IOException { var adapter = moshi.newBuilder().add(new ColorAdapter()).build().adapter(QualifiedValues.class); assertThat(adapter.fromJson("{\"value\":\"#ff0000\"}")) .isEqualTo(new QualifiedValues(16711680)); } public record QualifiedValues(@HexColor int value) {} @Retention(RUNTIME) @JsonQualifier @interface HexColor {} /** Converts strings like #ff0000 to the corresponding color ints. */ public static class ColorAdapter { @ToJson public String toJson(@HexColor int rgb) { return String.format("#%06x", rgb); } @FromJson @HexColor public int fromJson(String rgb) { return Integer.parseInt(rgb.substring(1), 16); } } public record GenericBoundedRecord(T value) {} @Test public void jsonName() throws IOException { var adapter = moshi.adapter(JsonName.class); assertThat(adapter.fromJson("{\"actualValue\":3}")).isEqualTo(new JsonName(3)); } public record JsonName(@Json(name = "actualValue") int value) {} /** * We had a bug where we were incorrectly wrapping exceptions thrown when delegating to the * JsonAdapters of component fields. */ @Test public void memberEncodeDecodeThrowsExceptionException() throws IOException { var throwingAdapter = new Object() { @ToJson void booleanToJson(JsonWriter writer, boolean value) throws IOException { throw new IOException("boom!"); } @FromJson boolean booleanFromJson(JsonReader reader) throws IOException { throw new IOException("boom!"); } }; var json = "{\"value\":true}"; Moshi throwingMoshi = this.moshi.newBuilder().add(throwingAdapter).build(); var adapter = throwingMoshi.adapter(BooleanRecord.class); try { adapter.fromJson(json); fail(); } catch (IOException expected) { assertThat(expected).hasMessageThat().isEqualTo("boom!"); } try { adapter.toJson(new Buffer(), new BooleanRecord(true)); fail(); } catch (IOException expected) { assertThat(expected).hasMessageThat().isEqualTo("boom!"); } } public record BooleanRecord(boolean value) {} @Test public void absentPrimitiveFails() throws IOException { var adapter = moshi.adapter(AbsentValues.class); try { adapter.fromJson("{\"s\":\"\"}"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Required value 'i' missing at $"); } } @Test public void nullPrimitiveFails() throws IOException { var adapter = moshi.adapter(AbsentValues.class); try { adapter.fromJson("{\"s\":\"\",\"i\":null}"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected an int but was NULL at path $.i"); } } @Test public void absentObjectIsNull() throws IOException { var adapter = moshi.adapter(AbsentValues.class); String json = "{\"i\":5}"; AbsentValues value = new AbsentValues(null, 5); assertThat(adapter.fromJson(json)).isEqualTo(value); assertThat(adapter.toJson(value)).isEqualTo(json); } @Test public void nullObjectIsNull() throws IOException { var adapter = moshi.adapter(AbsentValues.class); String json = "{\"i\":5,\"s\":null}"; AbsentValues value = new AbsentValues(null, 5); assertThat(adapter.fromJson(json)).isEqualTo(value); assertThat(adapter.toJson(value)).isEqualTo("{\"i\":5}"); } public record AbsentValues(String s, int i) {} } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/-JsonUtf8Reader.kt ================================================ /* * Copyright (C) 2010 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 * * https://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.squareup.moshi import com.squareup.moshi.internal.JsonScope import com.squareup.moshi.internal.JsonValueSource import com.squareup.moshi.internal.knownNotNull import java.math.BigDecimal import okio.Buffer import okio.BufferedSource import okio.ByteString import okio.ByteString.Companion.encodeUtf8 import okio.EOFException import okio.IOException import okio.buffer @Suppress("ktlint:standard:class-naming") // Hide this symbol from Java callers. internal class `-JsonUtf8Reader` : JsonReader { /** The input JSON. */ private val source: BufferedSource private val buffer: Buffer private var peeked = PEEKED_NONE /** * A peeked value that was composed entirely of digits with an optional leading dash. Positive * values may not have a leading 0. */ private var peekedLong = 0L /** The number of characters in a peeked number literal. */ private var peekedNumberLength = 0 /** * A peeked string that should be parsed on the next double, long or string. This is populated * before a numeric value is parsed and used if that parsing fails. */ private var peekedString: String? = null /** * If non-null, the most recent value read was [nextSource]. The caller may be mid-stream so it is * necessary to call [JsonValueSource.discard] to get to the end of the current JSON value before * proceeding. */ private var valueSource: JsonValueSource? = null constructor(source: BufferedSource) { this.source = source buffer = source.buffer pushScope(JsonScope.EMPTY_DOCUMENT) } /** Copy-constructor makes a deep copy for peeking. */ constructor(copyFrom: `-JsonUtf8Reader`) : super(copyFrom) { val sourcePeek = copyFrom.source.peek() source = sourcePeek buffer = sourcePeek.buffer peeked = copyFrom.peeked peekedLong = copyFrom.peekedLong peekedNumberLength = copyFrom.peekedNumberLength peekedString = copyFrom.peekedString // Make sure our buffer has as many bytes as the source's buffer. This is necessary because // JsonUtf8Reader assumes any data it has peeked (like the peekedNumberLength) are buffered. sourcePeek.require(copyFrom.buffer.size) } override fun beginArray() { val p = peekIfNone() if (p == PEEKED_BEGIN_ARRAY) { pushScope(JsonScope.EMPTY_ARRAY) pathIndices[stackSize - 1] = 0 peeked = PEEKED_NONE } else { throw JsonDataException("Expected BEGIN_ARRAY but was ${peek()} at path $path") } } override fun endArray() { val p = peekIfNone() if (p == PEEKED_END_ARRAY) { stackSize-- pathIndices[stackSize - 1]++ peeked = PEEKED_NONE } else { throw JsonDataException("Expected END_ARRAY but was ${peek()} at path $path") } } override fun beginObject() { val p = peekIfNone() if (p == PEEKED_BEGIN_OBJECT) { pushScope(JsonScope.EMPTY_OBJECT) peeked = PEEKED_NONE } else { throw JsonDataException("Expected BEGIN_OBJECT but was ${peek()} at path $path") } } override fun endObject() { val p = peekIfNone() if (p == PEEKED_END_OBJECT) { stackSize-- pathNames[stackSize] = null // Free the last path name so that it can be garbage collected! pathIndices[stackSize - 1]++ peeked = PEEKED_NONE } else { throw JsonDataException("Expected END_OBJECT but was ${peek()} at path $path") } } override fun hasNext(): Boolean { val p = peekIfNone() return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY && p != PEEKED_EOF } override fun peek(): Token { return when (peekIfNone()) { PEEKED_BEGIN_OBJECT -> Token.BEGIN_OBJECT PEEKED_END_OBJECT -> Token.END_OBJECT PEEKED_BEGIN_ARRAY -> Token.BEGIN_ARRAY PEEKED_END_ARRAY -> Token.END_ARRAY PEEKED_SINGLE_QUOTED_NAME, PEEKED_DOUBLE_QUOTED_NAME, PEEKED_UNQUOTED_NAME, PEEKED_BUFFERED_NAME -> Token.NAME PEEKED_TRUE, PEEKED_FALSE -> Token.BOOLEAN PEEKED_NULL -> Token.NULL PEEKED_SINGLE_QUOTED, PEEKED_DOUBLE_QUOTED, PEEKED_UNQUOTED, PEEKED_BUFFERED -> Token.STRING PEEKED_LONG, PEEKED_NUMBER -> Token.NUMBER PEEKED_EOF -> Token.END_DOCUMENT else -> throw AssertionError() } } private fun doPeek(): Int { val peekStack = scopes[stackSize - 1] when (peekStack) { JsonScope.EMPTY_ARRAY -> scopes[stackSize - 1] = JsonScope.NONEMPTY_ARRAY JsonScope.NONEMPTY_ARRAY -> { // Look for a comma before the next element. val c = nextNonWhitespace(true).toChar() buffer.readByte() // consume ']' or ','. when (c) { ']' -> { return setPeeked(PEEKED_END_ARRAY) } ';' -> checkLenient() /*no op*/ ',' -> Unit else -> throw syntaxError("Unterminated array") } } JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT -> { scopes[stackSize - 1] = JsonScope.DANGLING_NAME // Look for a comma before the next element. if (peekStack == JsonScope.NONEMPTY_OBJECT) { val c = nextNonWhitespace(true).toChar() buffer.readByte() // Consume '}' or ','. when (c) { '}' -> { return setPeeked(PEEKED_END_OBJECT) } /*no op*/ ',' -> Unit ';' -> checkLenient() else -> throw syntaxError("Unterminated object") } } val next = when (val c = nextNonWhitespace(true).toChar()) { '"' -> { buffer.readByte() // consume the '\"'. PEEKED_DOUBLE_QUOTED_NAME } '\'' -> { buffer.readByte() // consume the '\''. checkLenient() PEEKED_SINGLE_QUOTED_NAME } '}' -> if (peekStack != JsonScope.NONEMPTY_OBJECT) { buffer.readByte() // consume the '}'. PEEKED_END_OBJECT } else { throw syntaxError("Expected name") } else -> { checkLenient() if (isLiteral(c.code)) { PEEKED_UNQUOTED_NAME } else { throw syntaxError("Expected name") } } } peeked = next return next } JsonScope.DANGLING_NAME -> { scopes[stackSize - 1] = JsonScope.NONEMPTY_OBJECT // Look for a colon before the value. val c = nextNonWhitespace(true).toChar() buffer.readByte() // Consume ':'. when (c) { /*no op*/ ':' -> Unit '=' -> { checkLenient() if (source.request(1) && buffer[0].asChar() == '>') { buffer.readByte() // Consume '>'. } } else -> throw syntaxError("Expected ':'") } } JsonScope.EMPTY_DOCUMENT -> scopes[stackSize - 1] = JsonScope.NONEMPTY_DOCUMENT JsonScope.NONEMPTY_DOCUMENT -> { if (nextNonWhitespace(false) == -1) { return setPeeked(PEEKED_EOF) } else { checkLenient() } } JsonScope.STREAMING_VALUE -> { valueSource!!.discard() valueSource = null stackSize-- return doPeek() } else -> check(peekStack != JsonScope.CLOSED) { "JsonReader is closed" } } when (nextNonWhitespace(true).toChar()) { ']' -> { return when (peekStack) { JsonScope.EMPTY_ARRAY -> { buffer.readByte() // Consume ']'. setPeeked(PEEKED_END_ARRAY) } JsonScope.NONEMPTY_ARRAY -> { // In lenient mode, a 0-length literal in an array means 'null'. checkLenient() setPeeked(PEEKED_NULL) } else -> throw syntaxError("Unexpected value") } } // In lenient mode, a 0-length literal in an array means 'null'. ';', ',' -> return when (peekStack) { JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY -> { checkLenient() setPeeked(PEEKED_NULL) } else -> throw syntaxError("Unexpected value") } '\'' -> { checkLenient() buffer.readByte() // Consume '\''. return setPeeked(PEEKED_SINGLE_QUOTED) } '"' -> { buffer.readByte() // Consume '\"'. return setPeeked(PEEKED_DOUBLE_QUOTED) } '[' -> { buffer.readByte() // Consume '['. return setPeeked(PEEKED_BEGIN_ARRAY) } '{' -> { buffer.readByte() // Consume '{'. return setPeeked(PEEKED_BEGIN_OBJECT) } else -> Unit /* no-op */ } var result = peekKeyword() if (result != PEEKED_NONE) { return result } result = peekNumber() if (result != PEEKED_NONE) { return result } if (!isLiteral(buffer[0].toInt())) { throw syntaxError("Expected value") } checkLenient() return setPeeked(PEEKED_UNQUOTED) } private fun peekKeyword(): Int { // Figure out which keyword we're matching against by its first character. var c = buffer[0].asChar() val keyword: String val keywordUpper: String val peeking: Int when (c) { 't', 'T' -> { keyword = "true" keywordUpper = "TRUE" peeking = PEEKED_TRUE } 'f', 'F' -> { keyword = "false" keywordUpper = "FALSE" peeking = PEEKED_FALSE } 'n', 'N' -> { keyword = "null" keywordUpper = "NULL" peeking = PEEKED_NULL } else -> return PEEKED_NONE } // Confirm that chars [1..length) match the keyword. val length = keyword.length for (i in 1 until length) { val iAsLong = i.toLong() if (!source.request(iAsLong + 1)) { return PEEKED_NONE } c = buffer[iAsLong].asChar() if (c != keyword[i] && c != keywordUpper[i]) { return PEEKED_NONE } } if (source.request((length + 1).toLong()) && isLiteral(buffer[length.toLong()].toInt())) { return PEEKED_NONE // Don't match trues, falsey or nullsoft! } // We've found the keyword followed either by EOF or by a non-literal character. buffer.skip(length.toLong()) return setPeeked(peeking) } private fun peekNumber(): Int { var value = 0L // Negative to accommodate Long.MIN_VALUE more easily. var negative = false var fitsInLong = true var last = NUMBER_CHAR_NONE var i = 0L while (true) { if (!source.request(i + 1)) { break } when (val c = buffer[i].asChar()) { '-' -> { when (last) { NUMBER_CHAR_NONE -> { negative = true last = NUMBER_CHAR_SIGN i++ continue } NUMBER_CHAR_EXP_E -> { last = NUMBER_CHAR_EXP_SIGN i++ continue } } return PEEKED_NONE } '+' -> { if (last == NUMBER_CHAR_EXP_E) { last = NUMBER_CHAR_EXP_SIGN i++ continue } return PEEKED_NONE } 'e', 'E' -> { if (last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT) { last = NUMBER_CHAR_EXP_E i++ continue } return PEEKED_NONE } '.' -> { if (last == NUMBER_CHAR_DIGIT) { last = NUMBER_CHAR_DECIMAL i++ continue } return PEEKED_NONE } else -> { if (c !in '0'..'9') { if (!isLiteral(c.code)) break return PEEKED_NONE } when (last) { NUMBER_CHAR_SIGN, NUMBER_CHAR_NONE -> { value = -(c - '0').toLong() last = NUMBER_CHAR_DIGIT } NUMBER_CHAR_DIGIT -> { if (value == 0L) { return PEEKED_NONE // Leading '0' prefix is not allowed (since it could be octal). } val newValue = value * 10 - (c - '0').toLong() fitsInLong = fitsInLong and ((value > MIN_INCOMPLETE_INTEGER) || ((value == MIN_INCOMPLETE_INTEGER) && (newValue < value))) value = newValue } NUMBER_CHAR_DECIMAL -> last = NUMBER_CHAR_FRACTION_DIGIT NUMBER_CHAR_EXP_E, NUMBER_CHAR_EXP_SIGN -> last = NUMBER_CHAR_EXP_DIGIT } } } i++ } // We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER. return when { last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative) && (value != 0L || !negative) -> { peekedLong = if (negative) value else -value buffer.skip(i) setPeeked(PEEKED_LONG) } last == NUMBER_CHAR_DIGIT || last == NUMBER_CHAR_FRACTION_DIGIT || last == NUMBER_CHAR_EXP_DIGIT -> { peekedNumberLength = i.toInt() setPeeked(PEEKED_NUMBER) } else -> PEEKED_NONE } } @Throws(IOException::class) private fun isLiteral(c: Int): Boolean { return when (c.toChar()) { '/', '\\', ';', '#', '=' -> { checkLenient() // fall-through false } // 0x000C = \f '{', '}', '[', ']', ':', ',', ' ', '\t', '\u000C', '\r', '\n' -> false else -> true } } @Throws(IOException::class) override fun nextName(): String { val result = when (peekIfNone()) { PEEKED_UNQUOTED_NAME -> nextUnquotedValue() PEEKED_DOUBLE_QUOTED_NAME -> nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) PEEKED_SINGLE_QUOTED_NAME -> nextQuotedValue(SINGLE_QUOTE_OR_SLASH) PEEKED_BUFFERED_NAME -> { val name = peekedString!! peekedString = null name } else -> throw JsonDataException("Expected a name but was ${peek()} at path $path") } peeked = PEEKED_NONE pathNames[stackSize - 1] = result return result } @Throws(IOException::class) override fun selectName(options: Options): Int { val p = peekIfNone() if (p < PEEKED_SINGLE_QUOTED_NAME || p > PEEKED_BUFFERED_NAME) { return -1 } if (p == PEEKED_BUFFERED_NAME) { return findName(peekedString, options) } var result = source.select(options.doubleQuoteSuffix) if (result != -1) { peeked = PEEKED_NONE pathNames[stackSize - 1] = options.strings[result] return result } // The next name may be unnecessary escaped. Save the last recorded path name, so that we // can restore the peek state in case we fail to find a match. val lastPathName = pathNames[stackSize - 1] val nextName = nextName() result = findName(nextName, options) if (result == -1) { peeked = PEEKED_BUFFERED_NAME peekedString = nextName // We can't push the path further, make it seem like nothing happened. pathNames[stackSize - 1] = lastPathName } return result } @Throws(IOException::class) override fun skipName() { if (failOnUnknown) { // Capture the peeked value before nextName() since it will reset its value. val peeked = peek() nextName() // Move the path forward onto the offending name. throw JsonDataException("Cannot skip unexpected $peeked at $path") } val p = peekIfNone() when { p == PEEKED_UNQUOTED_NAME -> skipUnquotedValue() p == PEEKED_DOUBLE_QUOTED_NAME -> skipQuotedValue(DOUBLE_QUOTE_OR_SLASH) p == PEEKED_SINGLE_QUOTED_NAME -> skipQuotedValue(SINGLE_QUOTE_OR_SLASH) p != PEEKED_BUFFERED_NAME -> throw JsonDataException("Expected a name but was ${peek()} at path $path") } peeked = PEEKED_NONE pathNames[stackSize - 1] = "null" } /** * If `name` is in `options` this consumes it and returns its index. Otherwise this returns -1 and * no name is consumed. */ private fun findName(name: String?, options: Options): Int { val i = options.strings.indexOfFirst { it == name } return if (i > -1) { peeked = PEEKED_NONE pathNames[stackSize - 1] = name i } else { -1 } } override fun nextString(): String { val result = when (peekIfNone()) { PEEKED_UNQUOTED -> nextUnquotedValue() PEEKED_DOUBLE_QUOTED -> nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) PEEKED_SINGLE_QUOTED -> nextQuotedValue(SINGLE_QUOTE_OR_SLASH) PEEKED_BUFFERED -> { val buffered = peekedString!! peekedString = null buffered } PEEKED_LONG -> peekedLong.toString() PEEKED_NUMBER -> buffer.readUtf8(peekedNumberLength.toLong()) else -> throw JsonDataException("Expected a string but was ${peek()} at path $path") } peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return result } override fun selectString(options: Options): Int { val p = peekIfNone() if (p < PEEKED_SINGLE_QUOTED || p > PEEKED_BUFFERED) { return -1 } if (p == PEEKED_BUFFERED) { return findString(peekedString, options) } var result = source.select(options.doubleQuoteSuffix) if (result != -1) { peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return result } val nextString = nextString() result = findString(nextString, options) if (result == -1) { peeked = PEEKED_BUFFERED peekedString = nextString pathIndices[stackSize - 1]-- } return result } /** * If `string` is in `options` this consumes it and returns its index. Otherwise this returns -1 * and no string is consumed. */ private fun findString(string: String?, options: Options): Int { val i = options.strings.indexOfFirst { it == string } return if (i > -1) { peeked = PEEKED_NONE pathIndices[stackSize - 1]++ i } else { -1 } } override fun nextBoolean(): Boolean { return when (peekIfNone()) { PEEKED_TRUE -> { peeked = PEEKED_NONE pathIndices[stackSize - 1]++ true } PEEKED_FALSE -> { peeked = PEEKED_NONE pathIndices[stackSize - 1]++ false } else -> throw JsonDataException("Expected a boolean but was ${peek()} at path $path") } } override fun nextNull(): T? { val p = peekIfNone() return if (p == PEEKED_NULL) { peeked = PEEKED_NONE pathIndices[stackSize - 1]++ null } else { throw JsonDataException("Expected null but was ${peek()} at path $path") } } override fun nextDouble(): Double { val p = peekIfNone() if (p == PEEKED_LONG) { peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return peekedLong.toDouble() } val next = when (p) { PEEKED_NUMBER -> buffer.readUtf8(peekedNumberLength.toLong()).also { peekedString = it } PEEKED_DOUBLE_QUOTED -> nextQuotedValue(DOUBLE_QUOTE_OR_SLASH).also { peekedString = it } PEEKED_SINGLE_QUOTED -> nextQuotedValue(SINGLE_QUOTE_OR_SLASH).also { peekedString = it } PEEKED_UNQUOTED -> nextUnquotedValue().also { peekedString = it } PEEKED_BUFFERED -> { // PEEKED_BUFFERED means the value's been stored in peekedString knownNotNull(peekedString) } else -> throw JsonDataException("Expected a double but was " + peek() + " at path " + path) } peeked = PEEKED_BUFFERED val result = try { next.toDouble() } catch (_: NumberFormatException) { throw JsonDataException("Expected a double but was $next at path $path") } if (!lenient && (result.isNaN() || result.isInfinite())) { throw JsonEncodingException("JSON forbids NaN and infinities: $result at path $path") } peekedString = null peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return result } override fun nextLong(): Long { val p = peekIfNone() if (p == PEEKED_LONG) { peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return peekedLong } when { p == PEEKED_NUMBER -> peekedString = buffer.readUtf8(peekedNumberLength.toLong()) p == PEEKED_DOUBLE_QUOTED || p == PEEKED_SINGLE_QUOTED -> { peekedString = if (p == PEEKED_DOUBLE_QUOTED) { nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) } else { nextQuotedValue(SINGLE_QUOTE_OR_SLASH) } try { val result = peekedString!!.toLong() peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return result } catch (_: NumberFormatException) { // Fall back to parse as a BigDecimal below. } } p != PEEKED_BUFFERED -> { throw JsonDataException("Expected a long but was " + peek() + " at path " + path) } } peeked = PEEKED_BUFFERED val result = try { val asDecimal = BigDecimal(peekedString) asDecimal.longValueExact() } catch (_: NumberFormatException) { throw JsonDataException("Expected a long but was $peekedString at path $path") } catch (_: ArithmeticException) { throw JsonDataException("Expected a long but was $peekedString at path $path") } peekedString = null peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return result } /** * Returns the string up to but not including `quote`, unescaping any character escape sequences * encountered along the way. The opening quote should have already been read. This consumes the * closing quote, but does not include it in the returned string. * * @throws IOException if any Unicode escape sequences are malformed. */ private fun nextQuotedValue(runTerminator: ByteString): String { var builder: StringBuilder? = null while (true) { val index = source.indexOfElement(runTerminator) if (index == -1L) throw syntaxError("Unterminated string") // If we've got an escape character, we're going to need a string builder. if (buffer[index].asChar() == '\\') { if (builder == null) builder = StringBuilder() builder.append(buffer.readUtf8(index)) buffer.readByte() // '\' builder.append(readEscapeCharacter()) continue } // If it isn't the escape character, it's the quote. Return the string. return if (builder == null) { buffer.readUtf8(index).also { buffer.readByte() // Consume the quote character. } } else { builder.append(buffer.readUtf8(index)) buffer.readByte() // Consume the quote character. builder.toString() } } } /** Returns an unquoted value as a string. */ private fun nextUnquotedValue(): String { val i = source.indexOfElement(UNQUOTED_STRING_TERMINALS) return if (i != -1L) buffer.readUtf8(i) else buffer.readUtf8() } private fun skipQuotedValue(runTerminator: ByteString) { while (true) { val index = source.indexOfElement(runTerminator) if (index == -1L) throw syntaxError("Unterminated string") val terminator = buffer[index].asChar() buffer.skip(index + 1) if (terminator == '\\') { readEscapeCharacter() } else { return } } } private fun skipUnquotedValue() { val i = source.indexOfElement(UNQUOTED_STRING_TERMINALS) buffer.skip(if (i != -1L) i else buffer.size) } override fun nextInt(): Int { val p = peekIfNone() if (p == PEEKED_LONG) { val result = peekedLong.toInt() if (peekedLong != result.toLong()) { // Make sure no precision was lost casting to 'int'. throw JsonDataException("Expected an int but was $peekedLong at path $path") } peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return result } val next: String = when (p) { PEEKED_NUMBER -> { buffer.readUtf8(peekedNumberLength.toLong()).also { peekedString = it } } PEEKED_DOUBLE_QUOTED, PEEKED_SINGLE_QUOTED -> { val next = if (p == PEEKED_DOUBLE_QUOTED) { nextQuotedValue(DOUBLE_QUOTE_OR_SLASH) } else { nextQuotedValue(SINGLE_QUOTE_OR_SLASH) } peekedString = next try { val result = next.toInt() peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return result } catch (_: NumberFormatException) { // Fall back to parse as a double below. next } } PEEKED_BUFFERED -> { // PEEKED_BUFFERED means the value's been stored in peekedString knownNotNull(peekedString) } else -> throw JsonDataException("Expected an int but was ${peek()} at path $path") } peeked = PEEKED_BUFFERED val asDouble = try { next.toDouble() } catch (_: NumberFormatException) { throw JsonDataException("Expected an int but was $next at path $path") } val result = asDouble.toInt() if (result.toDouble() != asDouble) { // Make sure no precision was lost casting to 'int'. throw JsonDataException("Expected an int but was $next at path $path") } peekedString = null peeked = PEEKED_NONE pathIndices[stackSize - 1]++ return result } override fun close() { peeked = PEEKED_NONE scopes[0] = JsonScope.CLOSED stackSize = 1 buffer.clear() source.close() } override fun skipValue() { if (failOnUnknown) { throw JsonDataException("Cannot skip unexpected ${peek()} at $path") } var count = 0 do { when (peekIfNone()) { PEEKED_BEGIN_ARRAY -> { pushScope(JsonScope.EMPTY_ARRAY) count++ } PEEKED_BEGIN_OBJECT -> { pushScope(JsonScope.EMPTY_OBJECT) count++ } PEEKED_END_ARRAY -> { count-- if (count < 0) { throw JsonDataException("Expected a value but was ${peek()} at path $path") } stackSize-- } PEEKED_END_OBJECT -> { count-- if (count < 0) { throw JsonDataException("Expected a value but was ${peek()} at path $path") } stackSize-- } PEEKED_UNQUOTED_NAME, PEEKED_UNQUOTED -> skipUnquotedValue() PEEKED_DOUBLE_QUOTED, PEEKED_DOUBLE_QUOTED_NAME -> skipQuotedValue(DOUBLE_QUOTE_OR_SLASH) PEEKED_SINGLE_QUOTED, PEEKED_SINGLE_QUOTED_NAME -> skipQuotedValue(SINGLE_QUOTE_OR_SLASH) PEEKED_NUMBER -> buffer.skip(peekedNumberLength.toLong()) PEEKED_EOF -> throw JsonDataException("Expected a value but was ${peek()} at path $path") } peeked = PEEKED_NONE } while (count != 0) pathIndices[stackSize - 1]++ pathNames[stackSize - 1] = "null" } override fun nextSource(): BufferedSource { val p = peekIfNone() var valueSourceStackSize = 0 val prefix = Buffer() var state = JsonValueSource.STATE_END_OF_JSON when (p) { PEEKED_BEGIN_ARRAY -> { prefix.writeUtf8("[") state = JsonValueSource.STATE_JSON valueSourceStackSize++ } PEEKED_BEGIN_OBJECT -> { prefix.writeUtf8("{") state = JsonValueSource.STATE_JSON valueSourceStackSize++ } PEEKED_DOUBLE_QUOTED -> { prefix.writeUtf8("\"") state = JsonValueSource.STATE_DOUBLE_QUOTED } PEEKED_SINGLE_QUOTED -> { prefix.writeUtf8("'") state = JsonValueSource.STATE_SINGLE_QUOTED } PEEKED_NUMBER, PEEKED_LONG, PEEKED_UNQUOTED -> prefix.writeUtf8(nextString()) PEEKED_TRUE -> prefix.writeUtf8("true") PEEKED_FALSE -> prefix.writeUtf8("false") PEEKED_NULL -> prefix.writeUtf8("null") PEEKED_BUFFERED -> { val string = nextString() JsonWriter.of(prefix).use { jsonWriter -> jsonWriter.value(string) } } else -> throw JsonDataException("Expected a value but was ${peek()} at path $path") } // Advance the path and clear peeked if we haven't already. if (peeked != PEEKED_NONE) { pathIndices[stackSize - 1]++ peeked = PEEKED_NONE } val nextSource = JsonValueSource(source, prefix, state, valueSourceStackSize) valueSource = nextSource pushScope(JsonScope.STREAMING_VALUE) return nextSource.buffer() } /** * Returns the next character in the stream that is neither whitespace nor a part of a comment. * When this returns, the returned character is always at `buffer.getByte(0)`. */ private fun nextNonWhitespace(throwOnEof: Boolean): Int { /* * This code uses ugly local variable 'p' to represent the 'pos' field. * Using locals rather than fields saves a few field reads for each * whitespace character in a pretty-printed document, resulting in a * 5% speedup. We need to flush 'p' to its field before any * (potentially indirect) call to fillBuffer() and reread 'p' after * any (potentially indirect) call to the same method. */ var p = 0L while (source.request(p + 1)) { val c = buffer[p++].asChar() when (c) { '\n', ' ', '\r', '\t' -> continue } buffer.skip(p - 1) when (c) { '/' -> { if (!source.request(2)) { return c.code } checkLenient() val peek = buffer[1] return when (peek.asChar()) { '*' -> { // skip a /* c-style comment */ buffer.readByte() // '/' buffer.readByte() // '*' if (!skipToEndOfBlockComment()) { throw syntaxError("Unterminated comment") } p = 0 continue } '/' -> { // skip a // end-of-line comment buffer.readByte() // '/' buffer.readByte() // '/' skipToEndOfLine() p = 0 continue } else -> c.code } } '#' -> { // Skip a # hash end-of-line comment. The JSON RFC doesn't specify this behaviour, but // it's // required to parse existing documents. checkLenient() skipToEndOfLine() p = 0 } else -> return c.code } } if (throwOnEof) { throw EOFException("End of input") } return -1 } private fun checkLenient() { if (!lenient) { throw syntaxError("Use JsonReader.setLenient(true) to accept malformed JSON") } } /** * Advances the position until after the next newline character. If the line is terminated by * "\r\n", the '\n' must be consumed as whitespace by the caller. */ private fun skipToEndOfLine() { val index = source.indexOfElement(LINEFEED_OR_CARRIAGE_RETURN) buffer.skip(if (index != -1L) index + 1 else buffer.size) } /** Skips through the next closing block comment. */ private fun skipToEndOfBlockComment(): Boolean { val index = source.indexOf(CLOSING_BLOCK_COMMENT) val found = index != -1L buffer.skip(if (found) index + CLOSING_BLOCK_COMMENT.size else buffer.size) return found } override fun peekJson(): JsonReader = `-JsonUtf8Reader`(this) override fun toString(): String = "JsonReader($source)" /** * Unescapes the character identified by the character or characters that immediately follow a * backslash. The backslash '\' should have already been read. This supports both unicode escapes * "u000A" and two-character escapes "\n". * * @throws IOException if any unicode escape sequences are malformed. */ private fun readEscapeCharacter(): Char { if (!source.request(1)) { throw syntaxError("Unterminated escape sequence") } return when (val escaped = buffer.readByte().asChar()) { 'u' -> { if (!source.request(4)) { throw EOFException("Unterminated escape sequence at path $path") } // Equivalent to Integer.parseInt(stringPool.get(buffer, pos, 4), 16); var result = 0.toChar() for (i in 0 until 4) { result = (result.code shl 4).toChar() result += when (val c = buffer[i.toLong()].asChar()) { in '0'..'9' -> c - '0' in 'a'..'f' -> c - 'a' + 10 in 'A'..'F' -> c - 'A' + 10 else -> throw syntaxError("\\u" + buffer.readUtf8(4)) } } buffer.skip(4) result } 't' -> '\t' 'b' -> '\b' 'n' -> '\n' 'r' -> '\r' /*\f*/ 'f' -> '\u000C' '\n', '\'', '"', '\\', '/' -> escaped else -> { if (!lenient) throw syntaxError("Invalid escape sequence: \\$escaped") escaped } } } override fun promoteNameToValue() { if (hasNext()) { peekedString = nextName() peeked = PEEKED_BUFFERED } } @Suppress("NOTHING_TO_INLINE") private inline fun peekIfNone(): Int { val p = peeked return if (p == PEEKED_NONE) doPeek() else p } @Suppress("NOTHING_TO_INLINE") private inline fun setPeeked(peekedType: Int): Int { peeked = peekedType return peekedType } companion object { private const val MIN_INCOMPLETE_INTEGER = Long.MIN_VALUE / 10 private val SINGLE_QUOTE_OR_SLASH = "'\\".encodeUtf8() private val DOUBLE_QUOTE_OR_SLASH = "\"\\".encodeUtf8() private val UNQUOTED_STRING_TERMINALS = "{}[]:, \n\t\r\u000C/\\;#=".encodeUtf8() private val LINEFEED_OR_CARRIAGE_RETURN = "\n\r".encodeUtf8() private val CLOSING_BLOCK_COMMENT = "*/".encodeUtf8() private const val PEEKED_NONE = 0 private const val PEEKED_BEGIN_OBJECT = 1 private const val PEEKED_END_OBJECT = 2 private const val PEEKED_BEGIN_ARRAY = 3 private const val PEEKED_END_ARRAY = 4 private const val PEEKED_TRUE = 5 private const val PEEKED_FALSE = 6 private const val PEEKED_NULL = 7 private const val PEEKED_SINGLE_QUOTED = 8 private const val PEEKED_DOUBLE_QUOTED = 9 private const val PEEKED_UNQUOTED = 10 /** When this is returned, the string value is stored in peekedString. */ private const val PEEKED_BUFFERED = 11 private const val PEEKED_SINGLE_QUOTED_NAME = 12 private const val PEEKED_DOUBLE_QUOTED_NAME = 13 private const val PEEKED_UNQUOTED_NAME = 14 private const val PEEKED_BUFFERED_NAME = 15 /** When this is returned, the integer value is stored in peekedLong. */ private const val PEEKED_LONG = 16 private const val PEEKED_NUMBER = 17 private const val PEEKED_EOF = 18 /* State machine when parsing numbers */ private const val NUMBER_CHAR_NONE = 0 private const val NUMBER_CHAR_SIGN = 1 private const val NUMBER_CHAR_DIGIT = 2 private const val NUMBER_CHAR_DECIMAL = 3 private const val NUMBER_CHAR_FRACTION_DIGIT = 4 private const val NUMBER_CHAR_EXP_E = 5 private const val NUMBER_CHAR_EXP_SIGN = 6 private const val NUMBER_CHAR_EXP_DIGIT = 7 @Suppress("NOTHING_TO_INLINE") private inline fun Byte.asChar(): Char = toInt().toChar() } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/-JsonUtf8Writer.kt ================================================ /* * Copyright (C) 2010 Google 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.squareup.moshi import com.squareup.moshi.internal.JsonScope import java.io.IOException import kotlin.Array import kotlin.AssertionError import kotlin.Boolean import kotlin.Char import kotlin.IllegalStateException import kotlin.Int import kotlin.Long import kotlin.Number import kotlin.String import kotlin.arrayOfNulls import kotlin.check import kotlin.code import kotlin.require import okio.Buffer import okio.BufferedSink import okio.Sink import okio.Timeout import okio.buffer @Suppress("ktlint:standard:class-naming") // Hide this symbol from Java callers. internal class `-JsonUtf8Writer`( /** The output data, containing at most one top-level array or object. */ private val sink: BufferedSink ) : JsonWriter() { /** The name/value separator; either ":" or ": ". */ private var separator = ":" private var deferredName: String? = null override var indent: String get() = super.indent set(value) { super.indent = value separator = if (value.isNotEmpty()) ": " else ":" } init { pushScope(JsonScope.EMPTY_DOCUMENT) } override fun beginArray(): JsonWriter { check(!promoteValueToName) { "Array cannot be used as a map key in JSON at path $path" } writeDeferredName() return open(JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY, '[') } override fun endArray() = close(JsonScope.EMPTY_ARRAY, JsonScope.NONEMPTY_ARRAY, ']') override fun beginObject(): JsonWriter { check(!promoteValueToName) { "Object cannot be used as a map key in JSON at path $path" } writeDeferredName() return open(JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT, '{') } override fun endObject(): JsonWriter { promoteValueToName = false return close(JsonScope.EMPTY_OBJECT, JsonScope.NONEMPTY_OBJECT, '}') } /** Enters a new scope by appending any necessary whitespace and the given bracket. */ private fun open(empty: Int, nonempty: Int, openBracket: Char): JsonWriter { val shouldCancelOpen = stackSize == flattenStackSize && (scopes[stackSize - 1] == empty || scopes[stackSize - 1] == nonempty) if (shouldCancelOpen) { // Cancel this open. Invert the flatten stack size until this is closed. flattenStackSize = flattenStackSize.inv() return this } beforeValue() checkStack() pushScope(empty) pathIndices[stackSize - 1] = 0 sink.writeByte(openBracket.code) return this } /** Closes the current scope by appending any necessary whitespace and the given bracket. */ private fun close(empty: Int, nonempty: Int, closeBracket: Char): JsonWriter { val context = peekScope() check(context == nonempty || context == empty) { "Nesting problem." } check(deferredName == null) { "Dangling name: $deferredName" } if (stackSize == flattenStackSize.inv()) { // Cancel this close. Restore the flattenStackSize so we're ready to flatten again! flattenStackSize = flattenStackSize.inv() return this } stackSize-- pathNames[stackSize] = null // Free the last path name so that it can be garbage collected! pathIndices[stackSize - 1]++ if (context == nonempty) { newline() } sink.writeByte(closeBracket.code) return this } override fun name(name: String): JsonWriter { check(stackSize != 0) { "JsonWriter is closed." } val context = peekScope() val isWritingObject = !((((context != JsonScope.EMPTY_OBJECT) && (context != JsonScope.NONEMPTY_OBJECT)) || (deferredName != null) || promoteValueToName)) check(isWritingObject) { "Nesting problem." } deferredName = name pathNames[stackSize - 1] = name return this } private fun writeDeferredName() { deferredName?.let { deferredName -> beforeName() sink.string(deferredName) this.deferredName = null } } override fun value(value: String?): JsonWriter = apply { if (value == null) { return nullValue() } if (promoteValueToName) { promoteValueToName = false return name(value) } writeDeferredName() beforeValue() sink.string(value) pathIndices[stackSize - 1]++ } override fun nullValue(): JsonWriter = apply { check(!promoteValueToName) { "null cannot be used as a map key in JSON at path $path" } if (deferredName != null) { if (serializeNulls) { writeDeferredName() } else { deferredName = null return this // skip the name and the value } } beforeValue() sink.writeUtf8("null") pathIndices[stackSize - 1]++ } override fun value(value: Boolean): JsonWriter = apply { check(!promoteValueToName) { "Boolean cannot be used as a map key in JSON at path $path" } writeDeferredName() beforeValue() sink.writeUtf8(if (value) "true" else "false") pathIndices[stackSize - 1]++ } override fun value(value: Boolean?): JsonWriter { return when { value != null -> value(value) // Call the non-nullable overload. else -> nullValue() } } override fun value(value: Double): JsonWriter = apply { require(isLenient || (!value.isNaN() && !value.isInfinite())) { "Numeric values must be finite, but was $value" } if (promoteValueToName) { promoteValueToName = false return name(value.toString()) } writeDeferredName() beforeValue() sink.writeUtf8(value.toString()) pathIndices[stackSize - 1]++ } override fun value(value: Long): JsonWriter = apply { if (promoteValueToName) { promoteValueToName = false return name(value.toString()) } writeDeferredName() beforeValue() sink.writeUtf8(value.toString()) pathIndices[stackSize - 1]++ } override fun value(value: Number?): JsonWriter = apply { if (value == null) { return nullValue() } val string = value.toString() val isFinite = isLenient || ((string != "-Infinity") && (string != "Infinity") && (string != "NaN")) require(isFinite) { "Numeric values must be finite, but was $value" } if (promoteValueToName) { promoteValueToName = false return name(string) } writeDeferredName() beforeValue() sink.writeUtf8(string) pathIndices[stackSize - 1]++ } override fun valueSink(): BufferedSink { check(!promoteValueToName) { "BufferedSink cannot be used as a map key in JSON at path $path" } writeDeferredName() beforeValue() pushScope(JsonScope.STREAMING_VALUE) return object : Sink { override fun write(source: Buffer, byteCount: Long) { sink.write(source, byteCount) } override fun close() { if (peekScope() != JsonScope.STREAMING_VALUE) { throw AssertionError() } stackSize-- // Remove STREAMING_VALUE from the stack. pathIndices[stackSize - 1]++ } override fun flush() = sink.flush() override fun timeout() = Timeout.NONE } .buffer() } /** Ensures all buffered data is written to the underlying [Sink] and flushes that writer. */ override fun flush() { check(stackSize != 0) { "JsonWriter is closed." } sink.flush() } /** * Flushes and closes this writer and the underlying [Sink]. * * @throws JsonDataException if the JSON document is incomplete. */ override fun close() { sink.close() val size = stackSize if ((size > 1) || ((size == 1) && (scopes[0] != JsonScope.NONEMPTY_DOCUMENT))) { throw IOException("Incomplete document") } stackSize = 0 } private fun newline() { val indent = _indent ?: return sink.writeByte('\n'.code) repeat(stackSize - 1) { sink.writeUtf8(indent) } } /** * Inserts any necessary separators and whitespace before a name. Also adjusts the stack to expect * the name's value. */ private fun beforeName() { val context = peekScope() if (context == JsonScope.NONEMPTY_OBJECT) { // first in object sink.writeByte(','.code) } else { check(context == JsonScope.EMPTY_OBJECT) { // not in an object! "Nesting problem." } } newline() replaceTop(JsonScope.DANGLING_NAME) } /** * Inserts any necessary separators and whitespace before a literal value, inline array, or inline * object. Also adjusts the stack to expect either a closing bracket or another element. */ private fun beforeValue() { val nextTop: Int when (peekScope()) { JsonScope.NONEMPTY_DOCUMENT -> { check(isLenient) { "JSON must have only one top-level value." } nextTop = JsonScope.NONEMPTY_DOCUMENT } JsonScope.EMPTY_DOCUMENT -> nextTop = JsonScope.NONEMPTY_DOCUMENT JsonScope.NONEMPTY_ARRAY -> { sink.writeByte(','.code) newline() nextTop = JsonScope.NONEMPTY_ARRAY } JsonScope.EMPTY_ARRAY -> { newline() nextTop = JsonScope.NONEMPTY_ARRAY } JsonScope.DANGLING_NAME -> { nextTop = JsonScope.NONEMPTY_OBJECT sink.writeUtf8(separator) } JsonScope.STREAMING_VALUE -> throw IllegalStateException("Sink from valueSink() was not closed") else -> throw IllegalStateException("Nesting problem.") } replaceTop(nextTop) } companion object { /** * From RFC 7159, "All Unicode characters may be placed within the quotation marks except for * the characters that must be escaped: quotation mark, reverse solidus, and the control * characters (U+0000 through U+001F)." * * We also escape '\u2028' and '\u2029', which JavaScript interprets as newline characters. This * prevents eval() from failing with a syntax error. * http://code.google.com/p/google-gson/issues/detail?id=341 */ private val REPLACEMENT_CHARS: Array = arrayOfNulls(128) init { for (i in 0..0x1f) { REPLACEMENT_CHARS[i] = String.format("\\u%04x", i) } REPLACEMENT_CHARS['"'.code] = "\\\"" REPLACEMENT_CHARS['\\'.code] = "\\\\" REPLACEMENT_CHARS['\t'.code] = "\\t" REPLACEMENT_CHARS['\b'.code] = "\\b" REPLACEMENT_CHARS['\n'.code] = "\\n" REPLACEMENT_CHARS['\r'.code] = "\\r" REPLACEMENT_CHARS['\u000C'.code] = "\\f" } /** * Writes `value` as a string literal to `sink`. This wraps the value in double quotes and * escapes those characters that require it. */ @JvmStatic fun BufferedSink.string(value: String) { val replacements = REPLACEMENT_CHARS writeByte('"'.code) var last = 0 val length = value.length for (i in 0 until length) { val c = value[i] var replacement: String? if (c.code < 128) { replacement = replacements[c.code] if (replacement == null) { continue } } else if (c == '\u2028') { replacement = "\\u2028" } else if (c == '\u2029') { replacement = "\\u2029" } else { continue } if (last < i) { writeUtf8(value, last, i) } writeUtf8(replacement) last = i + 1 } if (last < length) { writeUtf8(value, last, length) } writeByte('"'.code) } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/-JsonValueReader.kt ================================================ /* * 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 * * https://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.squareup.moshi import com.squareup.moshi.internal.JsonScope import com.squareup.moshi.internal.knownNotNull import java.math.BigDecimal import okio.Buffer import okio.BufferedSource /** * This class reads a JSON document by traversing a Java object comprising maps, lists, and JSON * primitives. It does depth-first traversal keeping a stack starting with the root object. During * traversal a stack tracks the current position in the document: * * The next element to act upon is on the top of the stack. * * When the top of the stack is a [List], calling [beginArray] replaces the list with a * [JsonIterator]. The first element of the iterator is pushed on top of the iterator. * * Similarly, when the top of the stack is a [Map], calling [beginObject] replaces the map with an * [JsonIterator] of its entries. The first element of the iterator is pushed on top of the * iterator. * * When the top of the stack is a [Map.Entry], calling [nextName] returns the entry's key and * replaces the entry with its value on the stack. * * When an element is consumed it is popped. If the new top of the stack has a non-exhausted * iterator, the next element of that iterator is pushed. * * If the top of the stack is an exhausted iterator, calling [endArray] or [endObject] will pop * it. */ @Suppress("ktlint:standard:class-naming") // Hide this symbol from Java callers. internal class `-JsonValueReader` : JsonReader { private var stack: Array constructor(root: Any?) { scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT stack = arrayOfNulls(32) stack[stackSize++] = root } /** Copy-constructor makes a deep copy for peeking. */ constructor(copyFrom: `-JsonValueReader`) : super(copyFrom) { stack = copyFrom.stack.clone() for (i in 0 until stackSize) { val element = stack[i] if (element is JsonIterator) { stack[i] = element.clone() } } } override fun beginArray() { val peeked = require>(Token.BEGIN_ARRAY) val iterator = JsonIterator(Token.END_ARRAY, peeked.toTypedArray(), 0) stack[stackSize - 1] = iterator scopes[stackSize - 1] = JsonScope.EMPTY_ARRAY pathIndices[stackSize - 1] = 0 // If the iterator isn't empty push its first value onto the stack. if (iterator.hasNext()) { push(iterator.next()) } } override fun endArray() { val peeked = require(Token.END_ARRAY) if (peeked.endToken != Token.END_ARRAY || peeked.hasNext()) { throw typeMismatch(peeked, Token.END_ARRAY) } remove() } override fun beginObject() { val peeked = require>(Token.BEGIN_OBJECT) val iterator = JsonIterator(Token.END_OBJECT, peeked.entries.toTypedArray(), 0) stack[stackSize - 1] = iterator scopes[stackSize - 1] = JsonScope.EMPTY_OBJECT // If the iterator isn't empty push its first value onto the stack. if (iterator.hasNext()) { push(iterator.next()) } } override fun endObject() { val peeked = require(Token.END_OBJECT) if (peeked.endToken != Token.END_OBJECT || peeked.hasNext()) { throw typeMismatch(peeked, Token.END_OBJECT) } pathNames[stackSize - 1] = null remove() } override fun hasNext(): Boolean { if (stackSize == 0) return false val peeked = stack[stackSize - 1] return peeked !is Iterator<*> || peeked.hasNext() } override fun peek(): Token { if (stackSize == 0) return Token.END_DOCUMENT // If the top of the stack is an iterator, take its first element and push it on the stack. return when (val peeked = stack[stackSize - 1]) { is JsonIterator -> peeked.endToken is List<*> -> Token.BEGIN_ARRAY is Map<*, *> -> Token.BEGIN_OBJECT is Map.Entry<*, *> -> Token.NAME is String -> Token.STRING is Boolean -> Token.BOOLEAN is Number -> Token.NUMBER null -> Token.NULL else -> ifNotClosed(peeked) { throw typeMismatch(peeked, "a JSON value") } } } override fun nextName(): String { val peeked = require>(Token.NAME) // Swap the Map.Entry for its value on the stack and return its key. val result = stringKey(peeked) stack[stackSize - 1] = peeked.value pathNames[stackSize - 2] = result return result } override fun selectName(options: Options): Int { val peeked = require>(Token.NAME) val name = stringKey(peeked) for (i in options.strings.indices) { // Swap the Map.Entry for its value on the stack and return its key. if (options.strings[i] == name) { stack[stackSize - 1] = peeked.value pathNames[stackSize - 2] = name return i } } return -1 } override fun skipName() { if (failOnUnknown) { // Capture the peeked value before nextName() since it will reset its value. val peeked = peek() nextName() // Move the path forward onto the offending name. throw JsonDataException("Cannot skip unexpected $peeked at $path") } val (_, value) = require>(Token.NAME) // Swap the Map.Entry for its value on the stack. stack[stackSize - 1] = value pathNames[stackSize - 2] = "null" } override fun nextString(): String { return when (val peeked = if (stackSize != 0) stack[stackSize - 1] else null) { is String -> { remove() peeked } is Number -> { remove() peeked.toString() } else -> ifNotClosed(peeked) { throw typeMismatch(peeked, Token.STRING) } } } override fun selectString(options: Options): Int { val peeked = if (stackSize != 0) stack[stackSize - 1] else null if (peeked !is String) { ifNotClosed(peeked) { -1 } } for (i in options.strings.indices) { if (options.strings[i] == peeked) { remove() return i } } return -1 } override fun nextBoolean(): Boolean { val peeked = require(Token.BOOLEAN) remove() return peeked } override fun nextNull(): T? { requireNull() remove() return null } override fun nextDouble(): Double { val result = when (val peeked = require(Token.NUMBER)) { is Number -> peeked.toDouble() is String -> { try { peeked.toDouble() } catch (_: NumberFormatException) { throw typeMismatch(peeked, Token.NUMBER) } } else -> { throw typeMismatch(peeked, Token.NUMBER) } } if (!lenient && (result.isNaN() || result.isInfinite())) { throw JsonEncodingException("JSON forbids NaN and infinities: $result at path $path") } remove() return result } override fun nextLong(): Long { val result: Long = when (val peeked = require(Token.NUMBER)) { is Number -> peeked.toLong() is String -> try { peeked.toLong() } catch (_: NumberFormatException) { try { BigDecimal(peeked).longValueExact() } catch (_: NumberFormatException) { throw typeMismatch(peeked, Token.NUMBER) } } else -> throw typeMismatch(peeked, Token.NUMBER) } remove() return result } override fun nextInt(): Int { val result = when (val peeked = require(Token.NUMBER)) { is Number -> peeked.toInt() is String -> try { peeked.toInt() } catch (_: NumberFormatException) { try { BigDecimal(peeked).intValueExact() } catch (_: NumberFormatException) { throw typeMismatch(peeked, Token.NUMBER) } } else -> throw typeMismatch(peeked, Token.NUMBER) } remove() return result } override fun skipValue() { if (failOnUnknown) { throw JsonDataException("Cannot skip unexpected ${peek()} at $path") } // If this element is in an object clear out the key. if (stackSize > 1) { pathNames[stackSize - 2] = "null" } val skipped = if (stackSize != 0) stack[stackSize - 1] else null if (skipped is JsonIterator) { throw JsonDataException("Expected a value but was ${peek()} at path $path") } if (skipped is Map.Entry<*, *>) { // We're skipping a name. Promote the map entry's value. val entry = stack[stackSize - 1] as Map.Entry<*, *> stack[stackSize - 1] = entry.value } else if (stackSize > 0) { // We're skipping a value. remove() } else { throw JsonDataException("Expected a value but was ${peek()} at path $path") } } override fun nextSource(): BufferedSource { val value = readJsonValue() val result = Buffer() JsonWriter.of(result).use { jsonWriter -> jsonWriter.jsonValue(value) } return result } override fun peekJson(): JsonReader = `-JsonValueReader`(this) override fun promoteNameToValue() { if (hasNext()) { val name = nextName() push(name) } } override fun close() { stack.fill(null, 0, stackSize) stack[0] = JSON_READER_CLOSED scopes[0] = JsonScope.CLOSED stackSize = 1 } private fun push(newTop: Any?) { if (stackSize == stack.size) { if (stackSize == 256) { throw JsonDataException("Nesting too deep at $path") } scopes = scopes.copyOf(scopes.size * 2) pathNames = pathNames.copyOf(pathNames.size * 2) pathIndices = pathIndices.copyOf(pathIndices.size * 2) stack = stack.copyOf(stack.size * 2) } stack[stackSize++] = newTop } private inline fun require(expected: Token): T = knownNotNull(require(T::class.java, expected)) private fun requireNull() = require(Void::class.java, Token.NULL) /** * Returns the top of the stack which is required to be a `type`. Throws if this reader is closed, * or if the type isn't what was expected. */ private fun require(type: Class, expected: Token): T? { val peeked = if (stackSize != 0) stack[stackSize - 1] else null if (type.isInstance(peeked)) { return type.cast(peeked) } if (peeked == null && expected == Token.NULL) { return null } ifNotClosed(peeked) { throw typeMismatch(peeked, expected) } } private fun stringKey(entry: Map.Entry<*, *>): String { val name = entry.key if (name is String) return name throw typeMismatch(name, Token.NAME) } private inline fun ifNotClosed(peeked: Any?, body: () -> T): T { check(peeked !== JSON_READER_CLOSED) { "JsonReader is closed" } return body() } /** * Removes a value and prepares for the next. If we're iterating a map or list this advances the * iterator. */ private fun remove() { stackSize-- stack[stackSize] = null scopes[stackSize] = 0 // If we're iterating an array or an object push its next element on to the stack. if (stackSize > 0) { pathIndices[stackSize - 1]++ val parent = stack[stackSize - 1] if (parent is Iterator<*> && parent.hasNext()) { push(parent.next()) } } } internal class JsonIterator(val endToken: Token, val array: Array, var next: Int) : Iterator, Cloneable { override fun hasNext() = next < array.size override fun next() = array[next++] // No need to copy the array; it's read-only. public override fun clone() = JsonIterator(endToken, array, next) } private companion object { /** Sentinel object pushed on [JsonValueReader.stack] when the reader is closed. */ val JSON_READER_CLOSED = Any() } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/-JsonValueWriter.kt ================================================ /* * 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 * * https://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.squareup.moshi import com.squareup.moshi.internal.JsonScope.EMPTY_ARRAY import com.squareup.moshi.internal.JsonScope.EMPTY_DOCUMENT import com.squareup.moshi.internal.JsonScope.EMPTY_OBJECT import com.squareup.moshi.internal.JsonScope.NONEMPTY_DOCUMENT import com.squareup.moshi.internal.JsonScope.STREAMING_VALUE import com.squareup.moshi.internal.LinkedHashTreeMap import com.squareup.moshi.internal.knownNotNull import java.math.BigDecimal import okio.Buffer import okio.BufferedSink import okio.ForwardingSink import okio.IOException import okio.buffer /** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */ @Suppress("ktlint:standard:class-naming") // Hide this symbol from Java callers. internal class `-JsonValueWriter` : JsonWriter() { var stack = arrayOfNulls(32) private var deferredName: String? = null init { pushScope(EMPTY_DOCUMENT) } fun root(): Any? { val size = stackSize check(size <= 1 && (size != 1 || scopes[0] == NONEMPTY_DOCUMENT)) { "Incomplete document" } return stack[0] } override fun beginArray(): JsonWriter { check(!promoteValueToName) { "Array cannot be used as a map key in JSON at path $path" } if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_ARRAY) { // Cancel this open. Invert the flatten stack size until this is closed. flattenStackSize = flattenStackSize.inv() return this } checkStack() val list = mutableListOf() add(list) stack[stackSize] = list pathIndices[stackSize] = 0 pushScope(EMPTY_ARRAY) return this } override fun endArray(): JsonWriter { check(peekScope() == EMPTY_ARRAY) { "Nesting problem." } if (stackSize == flattenStackSize.inv()) { // Cancel this close. Restore the flattenStackSize so we're ready to flatten again! flattenStackSize = flattenStackSize.inv() return this } stackSize-- stack[stackSize] = null pathIndices[stackSize - 1]++ return this } override fun beginObject(): JsonWriter { check(!promoteValueToName) { "Object cannot be used as a map key in JSON at path $path" } if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_OBJECT) { // Cancel this open. Invert the flatten stack size until this is closed. flattenStackSize = flattenStackSize.inv() return this } checkStack() val map = LinkedHashTreeMap() add(map) stack[stackSize] = map pushScope(EMPTY_OBJECT) return this } override fun endObject(): JsonWriter { check(peekScope() == EMPTY_OBJECT) { "Nesting problem." } check(deferredName == null) { "Dangling name: $deferredName" } if (stackSize == flattenStackSize.inv()) { // Cancel this close. Restore the flattenStackSize so we're ready to flatten again! flattenStackSize = flattenStackSize.inv() return this } promoteValueToName = false stackSize-- stack[stackSize] = null pathNames[stackSize] = null // Free the last path name so that it can be garbage collected! pathIndices[stackSize - 1]++ return this } override fun name(name: String): JsonWriter { check(stackSize != 0) { "JsonWriter is closed." } check(peekScope() == EMPTY_OBJECT && deferredName == null && !promoteValueToName) { "Nesting problem." } deferredName = name pathNames[stackSize - 1] = name return this } override fun value(value: String?): JsonWriter { if (promoteValueToName) { promoteValueToName = false return name(value!!) } add(value) pathIndices[stackSize - 1]++ return this } override fun nullValue(): JsonWriter { check(!promoteValueToName) { "null cannot be used as a map key in JSON at path $path" } add(null) pathIndices[stackSize - 1]++ return this } override fun value(value: Boolean): JsonWriter { check(!promoteValueToName) { "Boolean cannot be used as a map key in JSON at path $path" } add(value) pathIndices[stackSize - 1]++ return this } override fun value(value: Boolean?): JsonWriter { check(!promoteValueToName) { "Boolean cannot be used as a map key in JSON at path $path" } add(value) pathIndices[stackSize - 1]++ return this } override fun value(value: Double): JsonWriter { require( isLenient || (!value.isNaN() && (value != Double.NEGATIVE_INFINITY) && (value != Double.POSITIVE_INFINITY)) ) { "Numeric values must be finite, but was $value" } if (promoteValueToName) { promoteValueToName = false return name(value.toString()) } add(value) pathIndices[stackSize - 1]++ return this } override fun value(value: Long): JsonWriter { if (promoteValueToName) { promoteValueToName = false return name(value.toString()) } add(value) pathIndices[stackSize - 1]++ return this } override fun value(value: Number?): JsonWriter = apply { when (value) { null -> nullValue() // If it's trivially converted to a long, do that. is Byte, is Short, is Int, is Long -> value(value.toLong()) // If it's trivially converted to a double, do that. is Float, is Double -> value(value.toDouble()) else -> { // Everything else gets converted to a BigDecimal. val bigDecimalValue = if (value is BigDecimal) value else BigDecimal(value.toString()) if (promoteValueToName) { promoteValueToName = false return name(bigDecimalValue.toString()) } add(bigDecimalValue) pathIndices[stackSize - 1]++ } } } override fun valueSink(): BufferedSink { check(!promoteValueToName) { "BufferedSink cannot be used as a map key in JSON at path $path" } check(peekScope() != STREAMING_VALUE) { "Sink from valueSink() was not closed" } pushScope(STREAMING_VALUE) val buffer = Buffer() return object : ForwardingSink(buffer) { override fun close() { if (peekScope() != STREAMING_VALUE || stack[stackSize] != null) { throw AssertionError() } stackSize-- // Remove STREAMING_VALUE from the stack. val value = JsonReader.of(buffer).readJsonValue() val serializeNulls = serializeNulls this@`-JsonValueWriter`.serializeNulls = true try { add(value) } finally { this@`-JsonValueWriter`.serializeNulls = serializeNulls } pathIndices[stackSize - 1]++ } } .buffer() } override fun close() { val size = stackSize if ((size > 1) || ((size == 1) && (scopes[0] != NONEMPTY_DOCUMENT))) { throw IOException("Incomplete document") } stackSize = 0 } override fun flush() { check(stackSize != 0) { "JsonWriter is closed." } } private fun add(newTop: Any?): `-JsonValueWriter` { val scope = peekScope() when { stackSize == 1 -> { check(scope == EMPTY_DOCUMENT) { "JSON must have only one top-level value." } scopes[stackSize - 1] = NONEMPTY_DOCUMENT stack[stackSize - 1] = newTop } scope == EMPTY_OBJECT && deferredName != null -> { if (newTop != null || serializeNulls) { // Our maps always have string keys and object values. @Suppress("UNCHECKED_CAST") val map = stack[stackSize - 1] as MutableMap // Safe to assume not null as this is single-threaded and smartcast just can't handle it val replaced = map.put(knownNotNull(deferredName), newTop) require(replaced == null) { "Map key '$deferredName' has multiple values at path $path: $replaced and $newTop" } } deferredName = null } scope == EMPTY_ARRAY -> { // Our lists always have object values. @Suppress("UNCHECKED_CAST") val list = stack[stackSize - 1] as MutableList list.add(newTop) } scope == STREAMING_VALUE -> throw IllegalStateException("Sink from valueSink() was not closed") else -> throw IllegalStateException("Nesting problem.") } return this } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/-MoshiKotlinExtensions.kt ================================================ /* * Copyright (C) 2019 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 * * https://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:Suppress("EXTENSION_SHADOWED_BY_MEMBER") package com.squareup.moshi import com.squareup.moshi.internal.NonNullJsonAdapter import com.squareup.moshi.internal.NullSafeJsonAdapter import com.squareup.moshi.internal.javaType import kotlin.reflect.KType import kotlin.reflect.typeOf /** * @return a [JsonAdapter] for [T], creating it if necessary. Note that while nullability of [T] * itself is handled, nested types (such as in generics) are not resolved. */ @Deprecated("Use the Moshi instance version instead", level = DeprecationLevel.HIDDEN) public inline fun Moshi.adapter(): JsonAdapter = adapter(typeOf()) @Deprecated("Use the Moshi instance version instead", level = DeprecationLevel.HIDDEN) public inline fun Moshi.Builder.addAdapter(adapter: JsonAdapter): Moshi.Builder = add(typeOf(), adapter) /** * @return a [JsonAdapter] for [ktype], creating it if necessary. Note that while nullability of * [ktype] itself is handled, nested types (such as in generics) are not resolved. */ @Deprecated("Use the Moshi instance version instead", level = DeprecationLevel.HIDDEN) public fun Moshi.adapter(ktype: KType): JsonAdapter { val adapter = adapter(ktype.javaType) val finalizedAdapter = if (adapter is NullSafeJsonAdapter<*> || adapter is NonNullJsonAdapter) { adapter } else if (ktype.isMarkedNullable) { adapter.nullSafe() } else { adapter.nonNull() } @Suppress("UNCHECKED_CAST") return finalizedAdapter as JsonAdapter } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/-MoshiKotlinTypesExtensions.kt ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi import com.squareup.moshi.internal.boxIfPrimitive import com.squareup.moshi.internal.javaType import java.lang.reflect.GenericArrayType import java.lang.reflect.Type import java.lang.reflect.WildcardType import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.typeOf /** Returns the raw [Class] type of this type. */ public val Type.rawType: Class<*> get() = Types.getRawType(this) /** * Checks if [this] contains [T]. Returns the subset of [this] without [T], or null if [this] does * not contain [T]. */ public inline fun Set.nextAnnotations(): Set? = Types.nextAnnotations(this, T::class.java) /** * Returns a type that represents an unknown type that extends [T]. For example, if [T] is * [CharSequence], this returns `out CharSequence`. If [T] is [Any], this returns `*`, which is * shorthand for `out Any?`. */ public inline fun subtypeOf(): WildcardType = subtypeOf(typeOf()) /** * Returns a type that represents an unknown type that extends [type]. For example, if [type] is * [CharSequence], this returns `out CharSequence`. If [type] is [Any], this returns `*`, which is * shorthand for `out Any?`. */ public fun subtypeOf(type: KType): WildcardType { var javaType = type.javaType if (javaType is Class<*>) { javaType = javaType.boxIfPrimitive() } return Types.subtypeOf(javaType) } /** * Returns a type that represents an unknown supertype of [T] bound. For example, if [T] is * [String], this returns `in String`. */ public inline fun supertypeOf(): WildcardType = supertypeOf(typeOf()) /** * Returns a type that represents an unknown supertype of [type] bound. For example, if [type] is * [String], this returns `in String`. */ public fun supertypeOf(type: KType): WildcardType { var javaType = type.javaType if (javaType is Class<*>) { javaType = javaType.boxIfPrimitive() } return Types.supertypeOf(javaType) } /** Returns a [GenericArrayType] with [this] as its [GenericArrayType.getGenericComponentType]. */ public fun KType.asArrayType(): GenericArrayType = javaType.asArrayType() /** Returns a [GenericArrayType] with [this] as its [GenericArrayType.getGenericComponentType]. */ public fun KClass<*>.asArrayType(): GenericArrayType = java.asArrayType() /** Returns a [GenericArrayType] with [this] as its [GenericArrayType.getGenericComponentType]. */ public fun Type.asArrayType(): GenericArrayType = Types.arrayOf(this) ================================================ FILE: moshi/src/main/java/com/squareup/moshi/FromJson.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.FUNCTION @Retention(RUNTIME) @Target(FUNCTION) public annotation class FromJson ================================================ FILE: moshi/src/main/java/com/squareup/moshi/Json.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi import kotlin.annotation.AnnotationRetention.RUNTIME /** * Customizes how a field is encoded as JSON. * * Although this annotation doesn't declare a [Target], it is only honored in the following * elements: * - **Java class fields** * - **Kotlin properties** for use with `moshi-kotlin` or `moshi-kotlin-codegen`. This includes both * properties declared in the constructor and properties declared as members. * * Users of the [AutoValue: Moshi Extension](https://github.com/rharter/auto-value-moshi) may also * use this annotation on abstract getters. */ @Retention(RUNTIME) @MustBeDocumented public annotation class Json( /** The name of the field when encoded as JSON. */ val name: String = UNSET_NAME, /** * If true, this field/property will be ignored. This is semantically similar to use of * `transient` on the JVM. * * **Note:** this has no effect in `enum` or `record` classes. */ val ignore: Boolean = false, ) { public companion object { /** The default value of [name]. Should only be used to check if it's been set. */ public const val UNSET_NAME: String = "\u0000" } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/JsonAdapter.kt ================================================ /* * 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 * * https://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.squareup.moshi import com.squareup.moshi.internal.NonNullJsonAdapter import com.squareup.moshi.internal.NullSafeJsonAdapter import java.lang.reflect.Type import javax.annotation.CheckReturnValue import kotlin.Throws import okio.Buffer import okio.BufferedSink import okio.BufferedSource import okio.IOException import org.intellij.lang.annotations.Language /** * Converts Java values to JSON, and JSON values to Java. * * JsonAdapter instances provided by Moshi are thread-safe, meaning multiple threads can safely use * a single instance concurrently. * * Custom JsonAdapter implementations should be designed to be thread-safe. */ public abstract class JsonAdapter { /** * Decodes a nullable instance of type [T] from the given [reader]. * * @throws JsonDataException when the data in a JSON document doesn't match the data expected by * the caller. */ @CheckReturnValue @Throws(IOException::class) public abstract fun fromJson(reader: JsonReader): T /** * Decodes a nullable instance of type [T] from the given [source]. * * @throws JsonDataException when the data in a JSON document doesn't match the data expected by * the caller. */ @CheckReturnValue @Throws(IOException::class) public fun fromJson(source: BufferedSource): T = fromJson(JsonReader.of(source)) /** * Decodes a nullable instance of type [T] from the given `string`. * * @throws JsonDataException when the data in a JSON document doesn't match the data expected by * the caller. */ @CheckReturnValue @Throws(IOException::class) public fun fromJson(@Language("JSON") string: String): T { val reader = JsonReader.of(Buffer().writeUtf8(string)) val result = fromJson(reader) if (!isLenient && reader.peek() != JsonReader.Token.END_DOCUMENT) { throw JsonDataException("JSON document was not fully consumed.") } return result } /** Encodes the given [value] with the given [writer]. */ @Throws(IOException::class) public abstract fun toJson(writer: JsonWriter, value: T) @Throws(IOException::class) public fun toJson(sink: BufferedSink, value: T) { val writer = JsonWriter.of(sink) toJson(writer, value) } /** Encodes the given [value] into a String and returns it. */ @CheckReturnValue public fun toJson(value: T): String { val buffer = Buffer() try { toJson(buffer, value) } catch (e: IOException) { throw AssertionError(e) // No I/O writing to a Buffer. } return buffer.readUtf8() } /** * Encodes [value] as a Java value object comprised of maps, lists, strings, numbers, booleans, * and nulls. * * Values encoded using `value(double)` or `value(long)` are modeled with the corresponding boxed * type. Values encoded using `value(Number)` are modeled as a [Long] for boxed integer types * ([Byte], [Short], [Integer], and [Long]), as a [Double] for boxed floating point types ([Float] * and [Double]), and as a [java.math.BigDecimal] for all other types. */ @CheckReturnValue public fun toJsonValue(value: T): Any? { val writer = `-JsonValueWriter`() return try { toJson(writer, value) writer.root() } catch (e: IOException) { throw AssertionError(e) // No I/O writing to an object. } } /** * Decodes a Java value object from [value], which must be comprised of maps, lists, strings, * numbers, booleans and nulls. */ @CheckReturnValue public fun fromJsonValue(value: Any?): T { val reader = `-JsonValueReader`(value) return try { fromJson(reader) } catch (e: IOException) { throw AssertionError(e) // No I/O reading from an object. } } /** * Returns a JSON adapter equal to this JSON adapter, but that serializes nulls when encoding * JSON. */ @CheckReturnValue public fun serializeNulls(): JsonAdapter { val delegate: JsonAdapter = this return object : JsonAdapter() { override fun fromJson(reader: JsonReader) = delegate.fromJson(reader) override fun toJson(writer: JsonWriter, value: T) { val serializeNulls = writer.serializeNulls writer.serializeNulls = true try { delegate.toJson(writer, value) } finally { writer.serializeNulls = serializeNulls } } override val isLenient: Boolean get() = delegate.isLenient override fun toString() = "$delegate.serializeNulls()" } } /** * Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing * nulls. */ @CheckReturnValue public fun nullSafe(): JsonAdapter { @Suppress("UNCHECKED_CAST") return when (this) { is NullSafeJsonAdapter<*> -> this as JsonAdapter else -> NullSafeJsonAdapter(this) } } /** * Returns a JSON adapter equal to this JSON adapter, but that refuses null values. If null is * read or written this will throw a [JsonDataException]. * * Note that this adapter will not usually be invoked for absent values and so those must be * handled elsewhere. This should only be used to fail on explicit nulls. */ @CheckReturnValue public fun nonNull(): JsonAdapter { val finalizedAdapter = when (this) { is NonNullJsonAdapter<*> -> { @Suppress("UNCHECKED_CAST") this as JsonAdapter } else -> NonNullJsonAdapter(this) } return finalizedAdapter.unsafeNonNull() } /** * Internal cast-only nonNull for adapters that are known to be internally null-safe but don't * need to allow null inputs. */ internal fun unsafeNonNull(): JsonAdapter { @Suppress("UNCHECKED_CAST") return this as JsonAdapter } /** Returns a JSON adapter equal to this, but is lenient when reading and writing. */ @CheckReturnValue public fun lenient(): JsonAdapter { val delegate: JsonAdapter = this return object : JsonAdapter() { override fun fromJson(reader: JsonReader): T { val lenient = reader.lenient reader.lenient = true return try { delegate.fromJson(reader) } finally { reader.lenient = lenient } } override fun toJson(writer: JsonWriter, value: T) { val lenient = writer.isLenient writer.isLenient = true try { delegate.toJson(writer, value) } finally { writer.isLenient = lenient } } override val isLenient: Boolean get() = true override fun toString() = "$delegate.lenient()" } } /** * Returns a JSON adapter equal to this, but that throws a [JsonDataException] when * [unknown names and values][JsonReader.failOnUnknown] are encountered. This constraint applies * to both the top-level message handled by this type adapter as well as to nested messages. */ @CheckReturnValue public fun failOnUnknown(): JsonAdapter { val delegate: JsonAdapter = this return object : JsonAdapter() { override fun fromJson(reader: JsonReader): T { val skipForbidden = reader.failOnUnknown reader.failOnUnknown = true return try { delegate.fromJson(reader) } finally { reader.failOnUnknown = skipForbidden } } override fun toJson(writer: JsonWriter, value: T) { delegate.toJson(writer, value) } override val isLenient: Boolean get() = delegate.isLenient override fun toString() = "$delegate.failOnUnknown()" } } /** * Return a JSON adapter equal to this, but using `indent` to control how the result is formatted. * The `indent` string to be repeated for each level of indentation in the encoded document. If * `indent.isEmpty()` the encoded document will be compact. Otherwise the encoded document will be * more human-readable. * * @param indent a string containing only whitespace. */ @CheckReturnValue public fun indent(indent: String): JsonAdapter { val delegate: JsonAdapter = this return object : JsonAdapter() { override fun fromJson(reader: JsonReader): T { return delegate.fromJson(reader) } override fun toJson(writer: JsonWriter, value: T) { val originalIndent = writer.indent writer.indent = indent try { delegate.toJson(writer, value) } finally { writer.indent = originalIndent } } override val isLenient: Boolean get() = delegate.isLenient override fun toString() = "$delegate.indent(\"$indent\")" } } internal open val isLenient: Boolean get() = false public fun interface Factory { /** * Attempts to create an adapter for `type` annotated with `annotations`. This returns the * adapter if one was created, or null if this factory isn't capable of creating such an * adapter. * * Implementations may use [Moshi.adapter] to compose adapters of other types, or * [Moshi.nextAdapter] to delegate to the underlying adapter of the same type. */ @CheckReturnValue public fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/JsonClass.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi import java.lang.reflect.Type import kotlin.annotation.AnnotationRetention.RUNTIME /** Customizes how a type is encoded as JSON. */ @Retention(RUNTIME) @MustBeDocumented public annotation class JsonClass( /** * True to trigger the annotation processor to generate an adapter for this type. * * There are currently some restrictions on which types that can be used with generated adapters: * - The class must be implemented in Kotlin (unless using a custom generator, see [generator]). * - The class may not be an abstract class, an inner class, or a local class. * - All superclasses must be implemented in Kotlin. * - All properties must be public, protected, or internal. * - All properties must be either non-transient or have a default value. */ val generateAdapter: Boolean, /** * An optional custom generator tag used to indicate which generator should be used. If empty, * Moshi's annotation processor will generate an adapter for the annotated type. If not empty, * Moshi's processor will skip it and defer to a custom generator. This can be used to allow other * custom code generation tools to run and still allow Moshi to read their generated JsonAdapter * outputs. * * Requirements for generated adapter class signatures: * - The generated adapter must subclass [JsonAdapter] and be parameterized by this type. * - [Types.generatedJsonAdapterName] should be used for the fully qualified class name in order * for Moshi to correctly resolve and load the generated JsonAdapter. * - The first parameter must be a [Moshi] instance. * - If generic, a second [Array][Type] parameter should be declared to accept type * arguments. * * Example for a class "CustomType": * ``` * class CustomTypeJsonAdapter(moshi: Moshi, types: Array) : JsonAdapter() { * // ... * } * ``` * * To help ensure your own generator meets requirements above, you can use Moshi’s built-in * generator to create the API signature to get started, then make your own generator match that * expected signature. */ val generator: String = "", /** * Set to true to generate an adapter that "inlines" the single property's value directly in JSON * rather than wrapping it in an object. This is primarily useful for Kotlin value classes but can * technically be used with any classes that wrap a single value. * * For example, a class `@JvmInline value class UserId(val id: Int)` with `inline = true` will * serialize as just `123` rather than `{"id": 123}`. * * Requirements: * - The class must have exactly one non-transient/non-ignored property. * - The property cannot be nullable. */ val inline: Boolean = false, ) ================================================ FILE: moshi/src/main/java/com/squareup/moshi/JsonDataException.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi /** * Thrown when the data in a JSON document doesn't match the data expected by the caller. For * example, suppose the application expects a boolean but the JSON document contains a string. When * the call to [JsonReader.nextBoolean] is made, a `JsonDataException` is thrown. * * Exceptions of this type should be fixed by either changing the application code to accept the * unexpected JSON, or by changing the JSON to conform to the application's expectations. * * This exception may also be triggered if a document's nesting exceeds 31 levels. This depth is * sufficient for all practical applications, but shallow enough to avoid uglier failures like * [StackOverflowError]. */ public class JsonDataException : RuntimeException { public constructor() : super() public constructor(message: String?) : super(message) public constructor(cause: Throwable?) : super(cause) public constructor(message: String?, cause: Throwable?) : super(message, cause) } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/JsonEncodingException.kt ================================================ /* * Copyright (C) 2016 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 * * https://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.squareup.moshi import java.io.IOException /** Thrown when the data being parsed is not encoded as valid JSON. */ public class JsonEncodingException(message: String?) : IOException(message) ================================================ FILE: moshi/src/main/java/com/squareup/moshi/JsonQualifier.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.ANNOTATION_CLASS /** Annotates another annotation, causing it to specialize how values are encoded and decoded. */ @Target(ANNOTATION_CLASS) @Retention(RUNTIME) @MustBeDocumented public annotation class JsonQualifier ================================================ FILE: moshi/src/main/java/com/squareup/moshi/JsonReader.kt ================================================ /* * Copyright (C) 2010 Google 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.squareup.moshi import com.squareup.moshi.`-JsonUtf8Writer`.Companion.string import com.squareup.moshi.internal.JsonScope.getPath import javax.annotation.CheckReturnValue import okio.Buffer import okio.BufferedSource import okio.Closeable import okio.IOException import okio.Options as OkioOptions /** * Reads a JSON ([RFC 7159](http://www.ietf.org/rfc/rfc7159.txt)) encoded value as a stream of * tokens. This stream includes both literal values (strings, numbers, booleans, and nulls) as well * as the begin and end delimiters of objects and arrays. The tokens are traversed in depth-first * order, the same order that they appear in the JSON document. Within JSON objects, name/value * pairs are represented by a single token. * * ## Parsing JSON * * To create a recursive descent parser for your own JSON streams, first create an entry point * method that creates a `JsonReader`. * * Next, create handler methods for each structure in your JSON text. You'll need a method for each * object type and for each array type. * * Within **array handling** methods, first call [beginArray] to consume the array's opening * bracket. Then create a `while` loop that accumulates values, terminating when [hasNext] is * false. Finally, read the array's closing bracket by calling [endArray]. * * Within **object handling** methods, first call [beginObject] to consume the object's opening * brace. Then create a `while` loop that assigns values to local variables based on their name. * This loop should terminate when [hasNext] is false. Finally, read the object's closing brace by * calling [endObject]. * * When a nested object or array is encountered, delegate to the corresponding handler method. * * When an unknown name is encountered, strict parsers should fail with an exception. Lenient * parsers should call [skipValue] to recursively skip the value's nested tokens, which may * otherwise conflict. * * If a value may be null, you should first check using [peek]. Null literals can be consumed using * either [nextNull] or [skipValue]. * * ## Example * * Suppose we'd like to parse a stream of messages such as the following: * ```json * [ * { * "id": 912345678901, * "text": "How do I read a JSON stream in Java?", * "geo": null, * "user": { * "name": "json_newb", * "followers_count": 41 * } * }, * { * "id": 912345678902, * "text": "@json_newb just use JsonReader!", * "geo": [50.454722, -104.606667], * "user": { * "name": "jesse", * "followers_count": 2 * } * } * ] * ``` * * This code implements the parser for the above structure: * ```java * public List readJsonStream(BufferedSource source) throws IOException { * JsonReader reader = JsonReader.of(source); * try { * return readMessagesArray(reader); * } finally { * reader.close(); * } * } * * public List readMessagesArray(JsonReader reader) throws IOException { * List messages = new ArrayList(); * * reader.beginArray(); * while (reader.hasNext()) { * messages.add(readMessage(reader)); * } * reader.endArray(); * return messages; * } * * public Message readMessage(JsonReader reader) throws IOException { * long id = -1; * String text = null; * User user = null; * List geo = null; * * reader.beginObject(); * while (reader.hasNext()) { * String name = reader.nextName(); * if (name.equals("id")) { * id = reader.nextLong(); * } else if (name.equals("text")) { * text = reader.nextString(); * } else if (name.equals("geo") && reader.peek() != Token.NULL) { * geo = readDoublesArray(reader); * } else if (name.equals("user")) { * user = readUser(reader); * } else { * reader.skipValue(); * } * } * reader.endObject(); * return new Message(id, text, user, geo); * } * * public List readDoublesArray(JsonReader reader) throws IOException { * List doubles = new ArrayList(); * * reader.beginArray(); * while (reader.hasNext()) { * doubles.add(reader.nextDouble()); * } * reader.endArray(); * return doubles; * } * * public User readUser(JsonReader reader) throws IOException { * String username = null; * int followersCount = -1; * * reader.beginObject(); * while (reader.hasNext()) { * String name = reader.nextName(); * if (name.equals("name")) { * username = reader.nextString(); * } else if (name.equals("followers_count")) { * followersCount = reader.nextInt(); * } else { * reader.skipValue(); * } * } * reader.endObject(); * return new User(username, followersCount); * } * ``` * * ## Number Handling * * This reader permits numeric values to be read as strings and string values to be read as numbers. * For example, both elements of the JSON array `[1, "1"]` may be read using either [nextInt] or * [nextString]. This behavior is intended to prevent lossy numeric conversions: double is * JavaScript's only numeric type and very large values like `9007199254740993` cannot be * represented exactly on that platform. To minimize precision loss, extremely large values should * be written and read as strings in JSON. * * Each `JsonReader` may be used to read a single JSON stream. Instances of this class are not * thread safe. */ public sealed class JsonReader : Closeable { // The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will // grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is // prone to trigger StackOverflowErrors. @JvmField protected var stackSize: Int = 0 @JvmField protected var scopes: IntArray @JvmField protected var pathNames: Array @JvmField protected var pathIndices: IntArray /** * Returns true if this parser is liberal in what it accepts. * * ## Getting * True to accept non-spec compliant JSON. * * ## Setting * Configure this parser to be liberal in what it accepts. By default this parser is strict and * only accepts JSON as specified by [RFC 7159](http://www.ietf.org/rfc/rfc7159.txt). Setting the * parser to lenient causes it to ignore the following syntax errors: * * Streams that include multiple top-level values. With strict parsing, each stream must contain * exactly one top-level value. * * Numbers may be [NaNs][Double.isNaN] or [infinities][Double.isInfinite]. * * End of line comments starting with `//` or `#` and ending with a newline character. * * C-style comments starting with `/ *` and ending with `*``/`. Such comments may not be nested. * * Names that are unquoted or `'single quoted'`. * * Strings that are unquoted or `'single quoted'`. * * Array elements separated by `;` instead of `,`. * * Unnecessary array separators. These are interpreted as if null was the omitted value. * * Names and values separated by `=` or `=>` instead of `:`. * * Name/value pairs separated by `;` instead of `,`. */ @get:CheckReturnValue @get:JvmName("isLenient") public var lenient: Boolean = false /** * True to throw a [JsonDataException] on any attempt to call [skipValue]. * * ## Getting * Returns true if this parser forbids skipping names and values. * * ## Setting * Configure whether this parser throws a [JsonDataException] when [skipValue] is called. By * default this parser permits values to be skipped. * * Forbid skipping to prevent unrecognized values from being silently ignored. This option is * useful in development and debugging because it means a typo like "locatiom" will be detected * early. It's potentially harmful in production because it complicates revising a JSON schema. */ @get:JvmName("failOnUnknown") public var failOnUnknown: Boolean = false private var tags: MutableMap, Any>? = null protected constructor() { scopes = IntArray(32) pathNames = arrayOfNulls(32) pathIndices = IntArray(32) } protected constructor(copyFrom: JsonReader) { stackSize = copyFrom.stackSize scopes = copyFrom.scopes.clone() pathNames = copyFrom.pathNames.clone() pathIndices = copyFrom.pathIndices.clone() lenient = copyFrom.lenient failOnUnknown = copyFrom.failOnUnknown } protected fun pushScope(newTop: Int) { if (stackSize == scopes.size) { if (stackSize == 256) { throw JsonDataException("Nesting too deep at $path") } scopes = scopes.copyOf(scopes.size * 2) pathNames = pathNames.copyOf(pathNames.size * 2) pathIndices = pathIndices.copyOf(pathIndices.size * 2) } scopes[stackSize++] = newTop } /** * Throws a new IO exception with the given message and a context snippet with this reader's * content. */ @Suppress("NOTHING_TO_INLINE") @Throws(JsonEncodingException::class) protected inline fun syntaxError(message: String): JsonEncodingException { throw JsonEncodingException("$message at path $path") } protected fun typeMismatch(value: Any?, expected: Any): JsonDataException { return if (value == null) { JsonDataException("Expected $expected but was null at path $path") } else { JsonDataException( "Expected $expected but was $value, a ${value.javaClass.name}, at path $path" ) } } /** * Consumes the next token from the JSON stream and asserts that it is the beginning of a new * array. */ @Throws(IOException::class) public abstract fun beginArray() /** * Consumes the next token from the JSON stream and asserts that it is the end of the current * array. */ @Throws(IOException::class) public abstract fun endArray() /** * Consumes the next token from the JSON stream and asserts that it is the beginning of a new * object. */ @Throws(IOException::class) public abstract fun beginObject() /** * Consumes the next token from the JSON stream and asserts that it is the end of the current * object. */ @Throws(IOException::class) public abstract fun endObject() /** Returns true if the current array or object has another element. */ @CheckReturnValue @Throws(IOException::class) public abstract operator fun hasNext(): Boolean /** Returns the type of the next token without consuming it. */ @CheckReturnValue @Throws(IOException::class) public abstract fun peek(): Token /** * Returns the next token, a [property name][Token.NAME], and consumes it. * * @throws JsonDataException if the next token in the stream is not a property name. */ @CheckReturnValue @Throws(IOException::class) public abstract fun nextName(): String /** * If the next token is a [property name][Token.NAME] that's in [options], this consumes it and * returns its index. Otherwise, this returns -1 and no name is consumed. */ @CheckReturnValue @Throws(IOException::class) public abstract fun selectName(options: Options): Int /** * Skips the next token, consuming it. This method is intended for use when the JSON token stream * contains unrecognized or unhandled names. * * This throws a [JsonDataException] if this parser has been configured to [failOnUnknown] names. */ @Throws(IOException::class) public abstract fun skipName() /** * Returns the [string][Token.STRING] value of the next token, consuming it. If the next token is * a number, this method will return its string form. * * @throws JsonDataException if the next token is not a string or if this reader is closed. */ @Throws(IOException::class) public abstract fun nextString(): String /** * If the next token is a [string][Token.STRING] that's in [options], this consumes it and returns * its index. Otherwise, this returns -1 and no string is consumed. */ @CheckReturnValue @Throws(IOException::class) public abstract fun selectString(options: Options): Int /** * Returns the [boolean][Token.BOOLEAN] value of the next token, consuming it. * * @throws JsonDataException if the next token is not a boolean or if this reader is closed. */ @Throws(IOException::class) public abstract fun nextBoolean(): Boolean /** * Consumes the next token from the JSON stream and asserts that it is a literal null. Returns * null. * * @throws JsonDataException if the next token is not null or if this reader is closed. */ @Throws(IOException::class) public abstract fun nextNull(): T? /** * Returns the [double][Token.NUMBER] value of the next token, consuming it. If the next token is * a string, this method will attempt to parse it as a double using * [java.lang.Double.parseDouble]. * * @throws JsonDataException if the next token is not a literal value, or if the next literal * value cannot be parsed as a double, or is non-finite. */ @Throws(IOException::class) public abstract fun nextDouble(): Double /** * Returns the [long][Token.NUMBER] value of the next token, consuming it. If the next token is a * string, this method will attempt to parse it as a long. If the next token's numeric value * cannot be exactly represented by a Java `long`, this method throws. * * @throws JsonDataException if the next token is not a literal value, if the next literal value * cannot be parsed as a number, or exactly represented as a long. */ @Throws(IOException::class) public abstract fun nextLong(): Long /** * Returns the [int][Token.NUMBER] value of the next token, consuming it. If the next token is a * string, this method will attempt to parse it as an int. If the next token's numeric value * cannot be exactly represented by a Java `int`, this method throws. * * @throws JsonDataException if the next token is not a literal value, if the next literal value * cannot be parsed as a number, or exactly represented as an int. */ @Throws(IOException::class) public abstract fun nextInt(): Int /** * Returns the next value as a stream of UTF-8 bytes and consumes it. * * The following program demonstrates how JSON bytes are returned from an enclosing stream as * their original bytes, including their original whitespace: * ```java * String json = "{\"a\": [4, 5 ,6.0, {\"x\":7}, 8], \"b\": 9}"; * JsonReader reader = JsonReader.of(new Buffer().writeUtf8(json)); * reader.beginObject(); * assertThat(reader.nextName()).isEqualTo("a"); * try (BufferedSource bufferedSource = reader.nextSource()) { * assertThat(bufferedSource.readUtf8()).isEqualTo("[4, 5 ,6.0, {\"x\":7}, 8]"); * } * assertThat(reader.nextName()).isEqualTo("b"); * assertThat(reader.nextInt()).isEqualTo(9); * reader.endObject(); * ``` * * This reads an entire value: composite objects like arrays and objects are returned in their * entirety. The stream starts with the first character of the value (typically `[`, `{` * , or * `"`) and ends with the last character of the object (typically `]`, `}`, or `"`). * * The returned source may not be used after any other method on this `JsonReader` is called. For * example, the following code crashes with an exception: * ``` * JsonReader reader = ... * reader.beginArray(); * BufferedSource source = reader.nextSource(); * reader.endArray(); * source.readUtf8(); // Crash! * ``` * * The returned bytes are not validated. This method assumes the stream is well-formed JSON and * only attempts to find the value's boundary in the byte stream. It is the caller's * responsibility to check that the returned byte stream is a valid JSON value. * * Closing the returned source **does not** close this reader. */ @Throws(IOException::class) public abstract fun nextSource(): BufferedSource /** * Skips the next value recursively. If it is an object or array, all nested elements are skipped. * This method is intended for use when the JSON token stream contains unrecognized or unhandled * values. * * This throws a [JsonDataException] if this parser has been configured to [failOnUnknown] values. */ @Throws(IOException::class) public abstract fun skipValue() /** * Returns the value of the next token, consuming it. The result may be a string, number, boolean, * null, map, or list, according to the JSON structure. * * @throws JsonDataException if the next token is not a literal value, if a JSON object has a * duplicate key. * @see JsonWriter.jsonValue */ @Throws(IOException::class) public fun readJsonValue(): Any? { return when (peek()) { Token.BEGIN_ARRAY -> { return buildList { beginArray() while (hasNext()) { add(readJsonValue()) } endArray() } } Token.BEGIN_OBJECT -> { return buildMap { beginObject() while (hasNext()) { val name = nextName() val value = readJsonValue() val replaced = put(name, value) if (replaced != null) { throw JsonDataException( "Map key '$name' has multiple values at path $path: $replaced and $value" ) } } endObject() } } Token.STRING -> nextString() Token.NUMBER -> nextDouble() Token.BOOLEAN -> nextBoolean() Token.NULL -> nextNull() else -> throw IllegalStateException("Expected a value but was ${peek()} at path $path") } } /** * Returns a new `JsonReader` that can read data from this `JsonReader` without consuming it. The * returned reader becomes invalid once this one is next read or closed. * * For example, we can use `peekJson()` to lookahead and read the same data multiple times. * * ```java * Buffer buffer = new Buffer(); * buffer.writeUtf8("[123, 456, 789]") * * JsonReader jsonReader = JsonReader.of(buffer); * jsonReader.beginArray(); * jsonReader.nextInt(); // Returns 123, reader contains 456, 789 and ]. * * JsonReader peek = reader.peekJson(); * peek.nextInt() // Returns 456. * peek.nextInt() // Returns 789. * peek.endArray() * * jsonReader.nextInt() // Returns 456, reader contains 789 and ]. * ``` */ @CheckReturnValue public abstract fun peekJson(): JsonReader /** * Returns a [JsonPath](http://goessner.net/articles/JsonPath/) to the current location in the * JSON value. */ @get:CheckReturnValue public val path: String get() = getPath(stackSize, scopes, pathNames, pathIndices) /** Returns the tag value for the given class key. */ @CheckReturnValue public fun tag(clazz: Class): T? { @Suppress("UNCHECKED_CAST") return tags?.let { it[clazz] as T? } } /** Assigns the tag value using the given class key and value. */ public fun setTag(clazz: Class, value: T) { require(clazz.isAssignableFrom(value.javaClass)) { "Tag value must be of type ${clazz.name}" } val tagsToUse = tags ?: LinkedHashMap, Any>().also { tags = it } tagsToUse[clazz] = value } /** * Changes the reader to treat the next name as a string value. This is useful for map adapters so * that arbitrary type adapters can use [nextString] to read a name value. * * In this example, calling this method allows two sequential calls to [nextString]: * ```java * JsonReader reader = JsonReader.of(new Buffer().writeUtf8("{\"a\":\"b\"}")); * reader.beginObject(); * reader.promoteNameToValue(); * assertEquals("a", reader.nextString()); * assertEquals("b", reader.nextString()); * reader.endObject(); * ``` */ @Throws(IOException::class) public abstract fun promoteNameToValue() /** * A set of strings to be chosen with [selectName] or [selectString]. This prepares the encoded * values of the strings so they can be read directly from the input source. */ public class Options private constructor( internal val strings: Array, internal val doubleQuoteSuffix: OkioOptions, ) { /** Returns a copy of this [Option's][Options] strings. */ public fun strings(): List { return buildList(strings.size) { for (string in strings) { add(string) } } } public companion object { @CheckReturnValue @JvmStatic public fun of(vararg strings: String): Options { return try { val buffer = Buffer() val result = Array(strings.size) { i -> buffer.string(strings[i]) buffer.readByte() // Skip the leading double quote (but leave the trailing one). buffer.readByteString() } Options(strings.clone(), OkioOptions.of(*result)) } catch (e: IOException) { throw AssertionError(e) } } } } /** A structure, name, or value type in a JSON-encoded string. */ public enum class Token { /** * The opening of a JSON array. Written using [JsonWriter.beginArray] and read using * [JsonReader.beginArray]. */ BEGIN_ARRAY, /** * The closing of a JSON array. Written using [JsonWriter.endArray] and read using * [JsonReader.endArray]. */ END_ARRAY, /** * The opening of a JSON object. Written using [JsonWriter.beginObject] and read using * [JsonReader.beginObject]. */ BEGIN_OBJECT, /** * The closing of a JSON object. Written using [JsonWriter.endObject] and read using * [JsonReader.endObject]. */ END_OBJECT, /** * A JSON property name. Within objects, tokens alternate between names and their values. * Written using [JsonWriter.name] and read using [JsonReader.nextName] */ NAME, /** A JSON string. */ STRING, /** A JSON number represented in this API by a Java `double`, `long`, or `int`. */ NUMBER, /** A JSON `true` or `false`. */ BOOLEAN, /** A JSON `null`. */ NULL, /** * The end of the JSON stream. This sentinel value is returned by [JsonReader.peek] to signal * that the JSON-encoded value has no more tokens. */ END_DOCUMENT, } public companion object { /** Returns a new instance that reads UTF-8 encoded JSON from `source`. */ @CheckReturnValue @JvmStatic public fun of(source: BufferedSource): JsonReader { return `-JsonUtf8Reader`(source) } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/JsonWriter.kt ================================================ /* * Copyright (C) 2010 Google 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.squareup.moshi import com.squareup.moshi.internal.JsonScope import com.squareup.moshi.internal.JsonScope.EMPTY_ARRAY import com.squareup.moshi.internal.JsonScope.EMPTY_OBJECT import com.squareup.moshi.internal.JsonScope.NONEMPTY_ARRAY import com.squareup.moshi.internal.JsonScope.NONEMPTY_OBJECT import java.io.Flushable import javax.annotation.CheckReturnValue import kotlin.Throws import okio.BufferedSink import okio.BufferedSource import okio.Closeable import okio.IOException /** * Writes a JSON ([RFC 7159](http://www.ietf.org/rfc/rfc7159.txt)) encoded value to a stream, one * token at a time. The stream includes both literal values (strings, numbers, booleans and nulls) * as well as the begin and end delimiters of objects and arrays. * * ## Encoding JSON * * To encode your data as JSON, create a new `JsonWriter`. Each JSON document must contain one * top-level array or object. Call methods on the writer as you walk the structure's contents, * nesting arrays and objects as necessary: * * To write **arrays**, first call [beginArray]. Write each of the array's elements with the * appropriate [value] methods or by nesting other arrays and objects. Finally close the array * using [endArray]. * * To write **objects**, first call [beginObject]. Write each of the object's properties by * alternating calls to [name] with the property's value. Write property values with the * appropriate [value] method or by nesting other objects or arrays. Finally close the object * using [endObject]. * * ## Example * * Suppose we'd like to encode a stream of messages such as the following: * ```json * [ * { * "id": 912345678901, * "text": "How do I stream JSON in Java?", * "geo": null, * "user": { * "name": "json_newb", * "followers_count": 41 * } * }, * { * "id": 912345678902, * "text": "@json_newb just use JsonWriter!", * "geo": [ * 50.454722, * -104.606667 * ], * "user": { * "name": "jesse", * "followers_count": 2 * } * } * ] * ``` * * This code encodes the above structure: * ```java * public void writeJsonStream(BufferedSink sink, List messages) throws IOException { * JsonWriter writer = JsonWriter.of(sink); * writer.setIndent(" "); * writeMessagesArray(writer, messages); * writer.close(); * } * * public void writeMessagesArray(JsonWriter writer, List messages) throws IOException { * writer.beginArray(); * for (Message message : messages) { * writeMessage(writer, message); * } * writer.endArray(); * } * * public void writeMessage(JsonWriter writer, Message message) throws IOException { * writer.beginObject(); * writer.name("id").value(message.getId()); * writer.name("text").value(message.getText()); * if (message.getGeo() != null) { * writer.name("geo"); * writeDoublesArray(writer, message.getGeo()); * } else { * writer.name("geo").nullValue(); * } * writer.name("user"); * writeUser(writer, message.getUser()); * writer.endObject(); * } * * public void writeUser(JsonWriter writer, User user) throws IOException { * writer.beginObject(); * writer.name("name").value(user.getName()); * writer.name("followers_count").value(user.getFollowersCount()); * writer.endObject(); * } * * public void writeDoublesArray(JsonWriter writer, List doubles) throws IOException { * writer.beginArray(); * for (Double value : doubles) { * writer.value(value); * } * writer.endArray(); * } * ``` * * Each `JsonWriter` may be used to write a single JSON stream. Instances of this class are not * thread safe. Calls that would result in a malformed JSON string will fail with an * [IllegalStateException]. */ public sealed class JsonWriter : Closeable, Flushable { /** * The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will * grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is * prone to trigger StackOverflowErrors. */ @JvmField protected var stackSize: Int = 0 @JvmField protected var scopes: IntArray = IntArray(32) @JvmField protected var pathNames: Array = arrayOfNulls(32) @JvmField protected var pathIndices: IntArray = IntArray(32) /** * A string containing a full set of spaces for a single level of indentation, or null for no * pretty printing. */ @JvmField @Suppress("ktlint:standard:backing-property-naming") // Exposed to sealed subtypes. protected var _indent: String? = null /** * A string containing only whitespace, used for each level of indentation. If empty, the encoded * document will be compact. */ public open var indent: String get() = _indent.orEmpty() set(value) { _indent = value.ifEmpty { null } } /** * Configure this writer to relax its syntax rules. By default, this writer only emits well-formed * JSON as specified by [RFC 7159](http://www.ietf.org/rfc/rfc7159.txt). Setting the writer to * lenient permits the following: * - Top-level values of any type. With strict writing, the top-level value must be an object or * an array. * - Numbers may be [NaNs][Double.isNaN] or [infinities][Double.isInfinite]. * * Returns true if this writer has relaxed syntax rules. */ @get:CheckReturnValue public var isLenient: Boolean = false /** * Sets whether object members are serialized when their value is null. This has no impact on * array elements. The default is false. * * Returns true if object members are serialized when their value is null. This has no impact on * array elements. The default is false. */ @get:CheckReturnValue public var serializeNulls: Boolean = false @JvmField protected var promoteValueToName: Boolean = false /** * Controls the deepest stack size that has begin/end pairs flattened: * - If -1, no begin/end pairs are being suppressed. * - If positive, this is the deepest stack size whose begin/end pairs are eligible to be * flattened. * - If negative, it is the bitwise inverse (~) of the deepest stack size whose begin/end pairs * have been flattened. * * We differentiate between what layer would be flattened (positive) from what layer is being * flattened (negative) so that we don't double-flatten. * * To accommodate nested flattening we require callers to track the previous state when they * provide a new state. The previous state is returned from [beginFlatten] and restored with * [endFlatten]. */ @JvmField protected var flattenStackSize: Int = -1 private var tags: MutableMap, Any>? = null /** * Returns a [JsonPath](http://goessner.net/articles/JsonPath/) to the current location in the * JSON value. */ @get:CheckReturnValue public val path: String get() = JsonScope.getPath(stackSize, scopes, pathNames, pathIndices) /** Returns the scope on the top of the stack. */ protected fun peekScope(): Int { check(stackSize != 0) { "JsonWriter is closed." } return scopes[stackSize - 1] } /** Before pushing a value on the stack this confirms that the stack has capacity. */ protected fun checkStack(): Boolean { if (stackSize != scopes.size) return false if (stackSize == 256) { throw JsonDataException("Nesting too deep at $path: circular reference?") } scopes = scopes.copyOf(scopes.size * 2) pathNames = pathNames.copyOf(pathNames.size * 2) pathIndices = pathIndices.copyOf(pathIndices.size * 2) if (this is `-JsonValueWriter`) { stack = stack.copyOf(stack.size * 2) } return true } protected fun pushScope(newTop: Int) { scopes[stackSize++] = newTop } /** Replace the value on the top of the stack with the given value. */ protected fun replaceTop(topOfStack: Int) { scopes[stackSize - 1] = topOfStack } /** * Begins encoding a new array. Each call to this method must be paired with a call to [endArray]. * * @return this writer. */ @Throws(IOException::class) public abstract fun beginArray(): JsonWriter /** * Ends encoding the current array. * * @return this writer. */ @Throws(IOException::class) public abstract fun endArray(): JsonWriter /** * Begins encoding a new object. Each call to this method must be paired with a call to * [endObject]. * * @return this writer. */ @Throws(IOException::class) public abstract fun beginObject(): JsonWriter /** * Ends encoding the current object. * * @return this writer. */ @Throws(IOException::class) public abstract fun endObject(): JsonWriter /** * Encodes the property name. * * @param name the name of the forthcoming value. Must not be null. * @return this writer. */ @Throws(IOException::class) public abstract fun name(name: String): JsonWriter /** * Encodes `value`. * * @param value the literal string value, or null to encode a null literal. * @return this writer. */ @Throws(IOException::class) public abstract fun value(value: String?): JsonWriter /** * Encodes `null`. * * @return this writer. */ @Throws(IOException::class) public abstract fun nullValue(): JsonWriter /** * Encodes `value`. * * @return this writer. */ @Throws(IOException::class) public abstract fun value(value: Boolean): JsonWriter /** * Encodes `value`. * * @return this writer. */ @Throws(IOException::class) public abstract fun value(value: Boolean?): JsonWriter /** * Encodes `value`. * * @param value a finite value. May not be [NaNs][Double.isNaN] or * [infinities][Double.isInfinite]. * @return this writer. */ @Throws(IOException::class) public abstract fun value(value: Double): JsonWriter /** * Encodes `value`. * * @return this writer. */ @Throws(IOException::class) public abstract fun value(value: Long): JsonWriter /** * Encodes `value`. * * @param value a finite value. May not be [NaNs][Double.isNaN] or * [infinities][Double.isInfinite]. * @return this writer. */ @Throws(IOException::class) public abstract fun value(value: Number?): JsonWriter /** * Writes `source` directly without encoding its contents. Equivalent to * * ```java * try (BufferedSink sink = writer.valueSink()) { * source.readAll(sink): * } * ``` * * @see valueSink */ @Throws(IOException::class) public fun value(source: BufferedSource): JsonWriter { check(!promoteValueToName) { "BufferedSource cannot be used as a map key in JSON at path $path" } valueSink().use(source::readAll) return this } /** * Returns a [BufferedSink] into which arbitrary data can be written without any additional * encoding. You **must** call [BufferedSink.close] before interacting with this `JsonWriter` * instance again. * * Since no validation is performed, options like [serializeNulls] and other writer configurations * are not respected. */ @CheckReturnValue @Throws(IOException::class) public abstract fun valueSink(): BufferedSink /** * Encodes the value which may be a string, number, boolean, null, map, or list. * * @return this writer. * @see JsonReader.readJsonValue */ @Throws(IOException::class) public fun jsonValue(value: Any?): JsonWriter { when (value) { is Map<*, *> -> { beginObject() for ((k, v) in value) { requireNotNull(k) { "Map keys must be non-null" } require(k is String) { "Map keys must be of type String: ${k.javaClass.name}" } name(k) jsonValue(v) } endObject() } is List<*> -> { beginArray() for (element in value) { jsonValue(element) } endArray() } is String -> value(value) is Boolean -> value(value) is Double -> value(value) is Long -> value(value) is Number -> value(value) null -> nullValue() else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}") } return this } /** * Changes the writer to treat the next value as a string name. This is useful for map adapters so * that arbitrary type adapters can use [value] to write a name value. * * In this example, calling this method allows two sequential calls to [value] to produce the * object, `{"a": "b"}`. * * ```java * JsonWriter writer = JsonWriter.of(...); * writer.beginObject(); * writer.promoteValueToName(); * writer.value("a"); * writer.value("b"); * writer.endObject(); * ``` */ @Throws(IOException::class) public fun promoteValueToName() { val context = peekScope() check(context == NONEMPTY_OBJECT || context == EMPTY_OBJECT) { "Nesting problem." } promoteValueToName = true } /** * Cancels immediately-nested calls to [beginArray] or [beginObject] and their matching calls to * [endArray] or [endObject]. Use this to compose JSON adapters without nesting. * * For example, the following creates JSON with nested arrays: `[1,[2,3,4],5]`. * * ```java * JsonAdapter> integersAdapter = ... * public void writeNumbers(JsonWriter writer) { * writer.beginArray(); * writer.value(1); * integersAdapter.toJson(writer, Arrays.asList(2, 3, 4)); * writer.value(5); * writer.endArray(); * } * ``` * * With flattening we can create JSON with a single array `[1,2,3,4,5]`: * ```java * JsonAdapter> integersAdapter = ... * * public void writeNumbers(JsonWriter writer) { * writer.beginArray(); * int token = writer.beginFlatten(); * writer.value(1); * integersAdapter.toJson(writer, Arrays.asList(2, 3, 4)); * writer.value(5); * writer.endFlatten(token); * writer.endArray(); * } * ``` * * This method flattens arrays within arrays: * * Emit: `[1, [2, 3, 4], 5]` To produce: `[1, 2, 3, 4, 5]` * * It also flattens objects within objects. Do not call [name] before writing a flattened object. * * Emit: `{"a": 1, {"b": 2}, "c": 3}` To Produce: `{"a": 1, "b": 2, "c": 3}` * * Other combinations are permitted but do not perform flattening. For example, objects inside of * arrays are not flattened: * * Emit: ` [1, {"b": 2}, 3, [4, 5], 6]` To Produce: `[1, {"b": 2}, 3, 4, 5, 6]` * * This method returns an opaque token. Callers must match all calls to this method with a call to * [endFlatten] with the matching token. */ @CheckReturnValue public fun beginFlatten(): Int { val context = peekScope() check( context == NONEMPTY_OBJECT || context == EMPTY_OBJECT || context == NONEMPTY_ARRAY || context == EMPTY_ARRAY ) { "Nesting problem." } val token = flattenStackSize flattenStackSize = stackSize return token } /** Ends nested call flattening created by [beginFlatten]. */ public fun endFlatten(token: Int) { flattenStackSize = token } /** Returns the tag value for the given class key. */ @CheckReturnValue public fun tag(clazz: Class): T? { @Suppress("UNCHECKED_CAST") return tags?.get(clazz) as T? } /** Assigns the tag value using the given class key and value. */ public fun setTag(clazz: Class, value: T) { require(clazz.isAssignableFrom(value::class.java)) { "Tag value must be of type ${clazz.name}" } val localTags = tags ?: LinkedHashMap, Any>().also { tags = it } localTags[clazz] = value } public companion object { /** Returns a new instance that writes UTF-8 encoded JSON to `sink`. */ @JvmStatic @CheckReturnValue public fun of(sink: BufferedSink): JsonWriter = `-JsonUtf8Writer`(sink) } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/Moshi.kt ================================================ /* * 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 * * https://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.squareup.moshi import com.squareup.moshi.internal.AdapterMethodsFactory import com.squareup.moshi.internal.ArrayJsonAdapter import com.squareup.moshi.internal.ClassJsonAdapter import com.squareup.moshi.internal.CollectionJsonAdapter import com.squareup.moshi.internal.MapJsonAdapter import com.squareup.moshi.internal.NO_ANNOTATIONS import com.squareup.moshi.internal.NonNullJsonAdapter import com.squareup.moshi.internal.NullSafeJsonAdapter import com.squareup.moshi.internal.RecordJsonAdapter import com.squareup.moshi.internal.StandardJsonAdapters import com.squareup.moshi.internal.canonicalize import com.squareup.moshi.internal.createJsonQualifierImplementation import com.squareup.moshi.internal.isAnnotationPresent import com.squareup.moshi.internal.javaType import com.squareup.moshi.internal.removeSubtypeWildcard import com.squareup.moshi.internal.toStringWithAnnotations import java.lang.reflect.Type import javax.annotation.CheckReturnValue import kotlin.reflect.KType import kotlin.reflect.typeOf /** * Coordinates binding between JSON values and Java objects. * * Moshi instances are thread-safe, meaning multiple threads can safely use a single instance * concurrently. */ public class Moshi private constructor(builder: Builder) { private val factories = buildList { addAll(builder.factories) addAll(BUILT_IN_FACTORIES) } private val lastOffset = builder.lastOffset private val lookupChainThreadLocal = ThreadLocal() private val adapterCache = LinkedHashMap?>() /** Returns a JSON adapter for `type`, creating it if necessary. */ @CheckReturnValue public fun adapter(type: Type): JsonAdapter = adapter(type, NO_ANNOTATIONS) @CheckReturnValue public fun adapter(type: Class): JsonAdapter = adapter(type, NO_ANNOTATIONS) @CheckReturnValue public fun adapter(type: Type, annotationType: Class): JsonAdapter = adapter(type, setOf(createJsonQualifierImplementation(annotationType))) @CheckReturnValue public fun adapter( type: Type, vararg annotationTypes: Class, ): JsonAdapter { if (annotationTypes.size == 1) { return adapter(type, annotationTypes[0]) } val annotations = buildSet(annotationTypes.size) { for (annotationType in annotationTypes) { add(createJsonQualifierImplementation(annotationType)) } } return adapter(type, annotations) } @CheckReturnValue public fun adapter(type: Type, annotations: Set): JsonAdapter = adapter(type, annotations, fieldName = null) /** * @return a [JsonAdapter] for [T], creating it if necessary. Note that while nullability of [T] * itself is handled, nested types (such as in generics) are not resolved. */ @JvmOverloads @CheckReturnValue public inline fun adapter( annotations: Set = emptySet(), fieldName: String? = null, ): JsonAdapter = adapter(typeOf(), annotations, fieldName) /** * @return a [JsonAdapter] for [ktype], creating it if necessary. Note that while nullability of * [ktype] itself is handled, nested types (such as in generics) are not resolved. */ @JvmOverloads @CheckReturnValue public fun adapter( ktype: KType, annotations: Set = emptySet(), fieldName: String? = null, ): JsonAdapter { val adapter = adapter(ktype.javaType, annotations, fieldName) val finalizedAdapter = if (adapter is NullSafeJsonAdapter<*> || adapter is NonNullJsonAdapter) { adapter } else if (ktype.isMarkedNullable) { adapter.nullSafe() } else { adapter.nonNull() } @Suppress("UNCHECKED_CAST") return finalizedAdapter as JsonAdapter } /** * @param fieldName An optional field name associated with this type. The field name is used as a * hint for better adapter lookup error messages for nested structures. */ @CheckReturnValue public fun adapter( type: Type, annotations: Set, fieldName: String?, ): JsonAdapter { val cleanedType = type.canonicalize().removeSubtypeWildcard() // If there's an equivalent adapter in the cache, we're done! val cacheKey = cacheKey(cleanedType, annotations) synchronized(adapterCache) { val result = adapterCache[cacheKey] @Suppress("UNCHECKED_CAST") if (result != null) return result as JsonAdapter } var lookupChain = lookupChainThreadLocal.get() if (lookupChain == null) { lookupChain = LookupChain() lookupChainThreadLocal.set(lookupChain) } var success = false val adapterFromCall = lookupChain.push(cleanedType, fieldName, cacheKey) try { if (adapterFromCall != null) return adapterFromCall // Ask each factory to create the JSON adapter. for (i in factories.indices) { @Suppress("UNCHECKED_CAST") // Factories are required to return only matching JsonAdapters. val result = factories[i].create(cleanedType, annotations, this) as JsonAdapter? ?: continue // Success! Notify the LookupChain so it is cached and can be used by re-entrant calls. lookupChain.adapterFound(result) success = true return result } throw IllegalArgumentException( "No JsonAdapter for ${type.toStringWithAnnotations(annotations)}" ) } catch (e: IllegalArgumentException) { throw lookupChain.exceptionWithLookupStack(e) } finally { lookupChain.pop(success) } } @CheckReturnValue public fun nextAdapter( skipPast: JsonAdapter.Factory, type: Type, annotations: Set, ): JsonAdapter { val cleanedType = type.canonicalize().removeSubtypeWildcard() val skipPastIndex = factories.indexOf(skipPast) require(skipPastIndex != -1) { "Unable to skip past unknown factory $skipPast" } for (i in (skipPastIndex + 1) until factories.size) { @Suppress("UNCHECKED_CAST") // Factories are required to return only matching JsonAdapters. val result = factories[i].create(cleanedType, annotations, this) as JsonAdapter? if (result != null) return result } throw IllegalArgumentException( "No next JsonAdapter for ${cleanedType.toStringWithAnnotations(annotations)}" ) } /** Returns a new builder containing all custom factories used by the current instance. */ @CheckReturnValue public fun newBuilder(): Builder { val result = Builder() for (i in 0 until lastOffset) { result.add(factories[i]) } for (i in lastOffset until factories.size - BUILT_IN_FACTORIES.size) { result.addLast(factories[i]) } return result } /** Returns an opaque object that's equal if the type and annotations are equal. */ private fun cacheKey(type: Type, annotations: Set): Any { return if (annotations.isEmpty()) type else listOf(type, annotations) } public class Builder { internal val factories = mutableListOf() internal var lastOffset = 0 @CheckReturnValue public inline fun addAdapter(adapter: JsonAdapter): Builder = add(typeOf(), adapter) public fun add(type: KType, jsonAdapter: JsonAdapter): Builder = add(type.javaType, jsonAdapter) public fun add(type: Type, jsonAdapter: JsonAdapter): Builder = add(newAdapterFactory(type, jsonAdapter)) public fun add( type: Type, annotation: Class, jsonAdapter: JsonAdapter, ): Builder = apply { add(newAdapterFactory(type, annotation, jsonAdapter)) } public fun add(factory: JsonAdapter.Factory): Builder = apply { factories.add(lastOffset++, factory) } public fun add(adapter: Any): Builder = apply { add(AdapterMethodsFactory(adapter)) } @Suppress("unused") public fun addLast(type: Type, jsonAdapter: JsonAdapter): Builder = apply { addLast(newAdapterFactory(type, jsonAdapter)) } @Suppress("unused") public fun addLast( type: Type, annotation: Class, jsonAdapter: JsonAdapter, ): Builder = apply { addLast(newAdapterFactory(type, annotation, jsonAdapter)) } public fun addLast(factory: JsonAdapter.Factory): Builder = apply { factories.add(factory) } @Suppress("unused") public fun addLast(adapter: Any): Builder = apply { addLast(AdapterMethodsFactory(adapter)) } @CheckReturnValue public fun build(): Moshi = Moshi(this) } /** * A possibly-reentrant chain of lookups for JSON adapters. * * We keep track of the current stack of lookups: we may start by looking up the JSON adapter for * Employee, re-enter looking for the JSON adapter of HomeAddress, and re-enter again looking up * the JSON adapter of PostalCode. If any of these lookups fail we can provide a stack trace with * all of the lookups. * * Sometimes a JSON adapter factory depends on its own product; either directly or indirectly. To * make this work, we offer a JSON adapter stub while the final adapter is being computed. When it * is ready, we wire the stub to that finished adapter. This is necessary in self-referential * object models, such as an `Employee` class that has a `List` field for an * organization's management hierarchy. * * This class defers putting any JSON adapters in the cache until the topmost JSON adapter has * successfully been computed. That way we don't pollute the cache with incomplete stubs, or * adapters that may transitively depend on incomplete stubs. */ private inner class LookupChain { private val callLookups = mutableListOf>() private val stack = ArrayDeque>() private var exceptionAnnotated = false /** * Returns a JSON adapter that was already created for this call, or null if this is the first * time in this call that the cache key has been requested in this call. This may return a * lookup that isn't yet ready if this lookup is reentrant. */ fun push(type: Type, fieldName: String?, cacheKey: Any): JsonAdapter? { // Try to find a lookup with the same key for the same call. for (lookup in callLookups) { if (lookup.cacheKey == cacheKey) { @Suppress("UNCHECKED_CAST") val hit = lookup as Lookup stack += hit return if (hit.adapter != null) hit.adapter else hit } } // We might need to know about this cache key later in this call. Prepare for that. val lookup = Lookup(type, fieldName, cacheKey) callLookups += lookup stack += lookup return null } /** Sets the adapter result of the current lookup. */ fun adapterFound(result: JsonAdapter) { @Suppress("UNCHECKED_CAST") val currentLookup = stack.last() as Lookup currentLookup.adapter = result } /** * Completes the current lookup by removing a stack frame. * * @param success true if the adapter cache should be populated if this is the topmost lookup. */ fun pop(success: Boolean) { stack.removeLast() if (!stack.isEmpty()) return lookupChainThreadLocal.remove() if (success) { synchronized(adapterCache) { for (lookup in callLookups) { val replaced = adapterCache.put(lookup.cacheKey, lookup.adapter) if (replaced != null) { @Suppress("UNCHECKED_CAST") (lookup as Lookup).adapter = replaced as JsonAdapter adapterCache[lookup.cacheKey] = replaced } } } } } fun exceptionWithLookupStack(e: IllegalArgumentException): IllegalArgumentException { // Don't add the lookup stack to more than one exception; the deepest is sufficient. if (exceptionAnnotated) return e exceptionAnnotated = true val size = stack.size if (size == 1 && stack.first().fieldName == null) return e val errorMessage = buildString { append(e.message) for (lookup in stack.asReversed()) { append("\nfor ").append(lookup.type) if (lookup.fieldName != null) { append(' ').append(lookup.fieldName) } } } return IllegalArgumentException(errorMessage, e) } } /** This class implements `JsonAdapter` so it can be used as a stub for re-entrant calls. */ private class Lookup(val type: Type, val fieldName: String?, val cacheKey: Any) : JsonAdapter() { var adapter: JsonAdapter? = null override fun fromJson(reader: JsonReader) = withAdapter { fromJson(reader) } override fun toJson(writer: JsonWriter, value: T?) = withAdapter { toJson(writer, value) } private inline fun withAdapter(body: JsonAdapter.() -> R): R = checkNotNull(adapter) { "JsonAdapter isn't ready" }.body() override fun toString() = adapter?.toString() ?: super.toString() } internal companion object { @JvmField val BUILT_IN_FACTORIES: List = buildList(6) { add(StandardJsonAdapters) add(CollectionJsonAdapter.Factory) add(MapJsonAdapter.Factory) add(ArrayJsonAdapter.Factory) add(RecordJsonAdapter.Factory) add(ClassJsonAdapter.Factory) } fun newAdapterFactory(type: Type, jsonAdapter: JsonAdapter): JsonAdapter.Factory { return JsonAdapter.Factory { targetType, annotations, _ -> if (annotations.isEmpty() && Types.equals(type, targetType)) jsonAdapter else null } } fun newAdapterFactory( type: Type, annotation: Class, jsonAdapter: JsonAdapter, ): JsonAdapter.Factory { require(annotation.isAnnotationPresent(JsonQualifier::class.java)) { "$annotation does not have @JsonQualifier" } require(annotation.declaredMethods.isEmpty()) { "Use JsonAdapter.Factory for annotations with elements" } return JsonAdapter.Factory { targetType, annotations, _ -> if ( Types.equals(type, targetType) && annotations.size == 1 && annotations.isAnnotationPresent(annotation) ) { jsonAdapter } else { null } } } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/ToJson.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.FUNCTION @Retention(RUNTIME) @Target(FUNCTION) public annotation class ToJson ================================================ FILE: moshi/src/main/java/com/squareup/moshi/Types.kt ================================================ /* * Copyright (C) 2008 Google 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. */ @file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") package com.squareup.moshi import com.squareup.moshi.internal.EMPTY_TYPE_ARRAY import com.squareup.moshi.internal.GenericArrayTypeImpl import com.squareup.moshi.internal.ParameterizedTypeImpl import com.squareup.moshi.internal.WildcardTypeImpl import com.squareup.moshi.internal.getSupertype import java.lang.annotation.Annotation as JavaAnnotation import java.lang.reflect.Array import java.lang.reflect.GenericArrayType import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.lang.reflect.TypeVariable import java.lang.reflect.WildcardType import java.util.Collections import javax.annotation.CheckReturnValue /** Factory methods for types. */ @CheckReturnValue public object Types { /** * Resolves the generated [JsonAdapter] fully qualified class name for a given [clazz]. This is * the same lookup logic used by both the Moshi code generation as well as lookup for any * JsonClass-annotated classes. This can be useful if generating your own JsonAdapters without * using Moshi's first party code gen. * * @param clazz the class to calculate a generated JsonAdapter name for. * @return the resolved fully qualified class name to the expected generated JsonAdapter class. * Note that this name will always be a top-level class name and not a nested class. */ @JvmStatic public fun generatedJsonAdapterName(clazz: Class<*>): String { if (clazz.getAnnotation(JsonClass::class.java) == null) { throw IllegalArgumentException("Class does not have a JsonClass annotation: $clazz") } return generatedJsonAdapterName(clazz.name) } /** * Resolves the generated [JsonAdapter] fully qualified class name for a given [ ] `className`. * This is the same lookup logic used by both the Moshi code generation as well as lookup for any * JsonClass-annotated classes. This can be useful if generating your own JsonAdapters without * using Moshi's first party code gen. * * @param className the fully qualified class to calculate a generated JsonAdapter name for. * @return the resolved fully qualified class name to the expected generated JsonAdapter class. * Note that this name will always be a top-level class name and not a nested class. */ @JvmStatic public fun generatedJsonAdapterName(className: String): String { return "${className.replace("$", "_")}JsonAdapter" } /** * Checks if `annotations` contains `jsonQualifier`. Returns the subset of `annotations` without * `jsonQualifier`, or null if `annotations` does not contain `jsonQualifier`. */ @JvmStatic public fun nextAnnotations( annotations: Set, jsonQualifier: Class, ): Set? { require(jsonQualifier.isAnnotationPresent(JsonQualifier::class.java)) { "$jsonQualifier is not a JsonQualifier." } if (annotations.isEmpty()) { return null } for (annotation in annotations) { if ((jsonQualifier == (annotation as JavaAnnotation).annotationType())) { val delegateAnnotations = LinkedHashSet(annotations) delegateAnnotations.remove(annotation) return Collections.unmodifiableSet(delegateAnnotations) } } return null } /** * Returns a new parameterized type, applying `typeArguments` to `rawType`. Use this method if * `rawType` is not enclosed in another type. */ @JvmStatic public fun newParameterizedType(rawType: Type, vararg typeArguments: Type): ParameterizedType { require(typeArguments.isNotEmpty()) { "Missing type arguments for $rawType" } return ParameterizedTypeImpl(null, rawType, typeArguments) } /** * Returns a new parameterized type, applying `typeArguments` to `rawType`. Use this method if * `rawType` is enclosed in `ownerType`. */ @JvmStatic public fun newParameterizedTypeWithOwner( ownerType: Type?, rawType: Type, vararg typeArguments: Type, ): ParameterizedType { require(typeArguments.isNotEmpty()) { "Missing type arguments for $rawType" } return ParameterizedTypeImpl(ownerType, rawType, typeArguments) } /** Returns an array type whose elements are all instances of `componentType`. */ @JvmStatic public fun arrayOf(componentType: Type): GenericArrayType { return GenericArrayTypeImpl(componentType) } /** * Returns a type that represents an unknown type that extends `bound`. For example, if `bound` is * `CharSequence.class`, this returns `? extends CharSequence`. If `bound` is `Object.class`, this * returns `?`, which is shorthand for `? extends Object`. */ @JvmStatic public fun subtypeOf(bound: Type): WildcardType { val upperBounds = if (bound is WildcardType) { bound.upperBounds } else { arrayOf(bound) } return WildcardTypeImpl(upperBounds, EMPTY_TYPE_ARRAY) } /** * Returns a type that represents an unknown supertype of `bound`. For example, if `bound` is * `String.class`, this returns `? super String`. */ @JvmStatic public fun supertypeOf(bound: Type): WildcardType { val lowerBounds = if (bound is WildcardType) { bound.lowerBounds } else { arrayOf(bound) } return WildcardTypeImpl(arrayOf(Any::class.java), lowerBounds) } @JvmStatic public fun getRawType(type: Type?): Class<*> { return when (type) { is Class<*> -> { // type is a normal class. type } is ParameterizedType -> { // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either // but // suspects some pathological case related to nested classes exists. val rawType = type.rawType rawType as Class<*> } is GenericArrayType -> { val componentType = type.genericComponentType Array.newInstance(getRawType(componentType), 0).javaClass } is TypeVariable<*> -> { // We could use the variable's bounds, but that won't work if there are multiple. having a // raw // type that's more general than necessary is okay. Any::class.java } is WildcardType -> getRawType(type.upperBounds[0]) else -> { val className = type?.javaClass?.name?.toString() throw IllegalArgumentException( "Expected a Class, ParameterizedType, or GenericArrayType, but <$type> is of type $className" ) } } } /** * Returns the element type of this collection type. * * @throws IllegalArgumentException if this type is not a collection. */ @JvmStatic public fun collectionElementType(context: Type, contextRawType: Class<*>): Type { var collectionType: Type? = getSupertype(context, contextRawType, MutableCollection::class.java) if (collectionType is WildcardType) { collectionType = collectionType.upperBounds[0] } return if (collectionType is ParameterizedType) { collectionType.actualTypeArguments[0] } else { Any::class.java } } /** Returns true if `a` and `b` are equal. */ @JvmStatic public fun equals(a: Type?, b: Type?): Boolean { if (a === b) { return true // Also handles (a == null && b == null). } // This isn't a supported type. when (a) { is Class<*> -> { return if (b is GenericArrayType) { equals(a.componentType, b.genericComponentType) } else if (b is ParameterizedType && a.rawType == b.rawType) { // Class instance with generic info, from method return types return a.typeParameters.flatMap { it.bounds.toList() } == b.actualTypeArguments.toList() } else { a == b // Class already specifies equals(). } } is ParameterizedType -> { // Class instance with generic info, from method return types if (b is Class<*> && a.rawType == b.rawType) { return b.typeParameters.map { it.bounds }.toTypedArray().flatten() == a.actualTypeArguments.toList() } if (b !is ParameterizedType) return false val aTypeArguments = if (a is ParameterizedTypeImpl) { a.typeArguments } else { a.actualTypeArguments } val bTypeArguments = if (b is ParameterizedTypeImpl) { b.typeArguments } else { b.actualTypeArguments } return (equals(a.ownerType, b.ownerType) && (a.rawType == b.rawType) && aTypeArguments.contentEquals(bTypeArguments)) } is GenericArrayType -> { if (b is Class<*>) { return equals(b.componentType, a.genericComponentType) } if (b !is GenericArrayType) return false return equals(a.genericComponentType, b.genericComponentType) } is WildcardType -> { if (b !is WildcardType) return false return (a.upperBounds.contentEquals(b.upperBounds) && a.lowerBounds.contentEquals(b.lowerBounds)) } is TypeVariable<*> -> { if (b !is TypeVariable<*>) return false return (a.genericDeclaration === b.genericDeclaration && (a.name == b.name)) } else -> return false // This isn't a supported type. } } /** * @param clazz the target class to read the `fieldName` field annotations from. * @param fieldName the target field name on `clazz`. * @return a set of [JsonQualifier]-annotated [Annotation] instances retrieved from the targeted * field. Can be empty if none are found. */ @Deprecated( "This is no longer needed in Kotlin 1.6.0 (which has direct annotation instantiation) and is obsolete." ) @JvmStatic public fun getFieldJsonQualifierAnnotations(clazz: Class<*>, fieldName: String): Set { try { val field = clazz.getDeclaredField(fieldName) field.isAccessible = true val fieldAnnotations = field.declaredAnnotations return buildSet(fieldAnnotations.size) { for (annotation in fieldAnnotations) { val hasJsonQualifier = (annotation as JavaAnnotation) .annotationType() .isAnnotationPresent(JsonQualifier::class.java) if (hasJsonQualifier) { add(annotation) } } } } catch (e: NoSuchFieldException) { throw IllegalArgumentException( "Could not access field $fieldName on class ${clazz.canonicalName}", e, ) } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/AdapterMethodsFactory.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.internal import com.squareup.moshi.FromJson 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.ToJson import com.squareup.moshi.Types import java.io.IOException import java.lang.reflect.InvocationTargetException import java.lang.reflect.Method import java.lang.reflect.ParameterizedType import java.lang.reflect.Type internal class AdapterMethodsFactory( private val toAdapters: List, private val fromAdapters: List, ) : JsonAdapter.Factory { override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { val toAdapter = get(toAdapters, type, annotations) val fromAdapter = get(fromAdapters, type, annotations) if (toAdapter == null && fromAdapter == null) return null val delegate: JsonAdapter? = if (toAdapter == null || fromAdapter == null) { try { moshi.nextAdapter(this, type, annotations) } catch (e: IllegalArgumentException) { val missingAnnotation = if (toAdapter == null) "@ToJson" else "@FromJson" throw IllegalArgumentException( "No $missingAnnotation adapter for ${type.toStringWithAnnotations(annotations)}", e, ) } } else { null } toAdapter?.bind(moshi, this) fromAdapter?.bind(moshi, this) return object : JsonAdapter() { override fun toJson(writer: JsonWriter, value: Any?) { when { toAdapter == null -> knownNotNull(delegate).toJson(writer, value) !toAdapter.nullable && value == null -> writer.nullValue() else -> { try { toAdapter.toJson(moshi, writer, value) } catch (e: InvocationTargetException) { val cause = e.cause if (cause is IOException) throw cause throw JsonDataException("$cause at ${writer.path}", cause) } } } } override fun fromJson(reader: JsonReader): Any? { return when { fromAdapter == null -> knownNotNull(delegate).fromJson(reader) !fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL -> reader.nextNull() else -> { try { fromAdapter.fromJson(moshi, reader) } catch (e: InvocationTargetException) { val cause = e.cause if (cause is IOException) throw cause throw JsonDataException("$cause at ${reader.path}", cause) } } } } override fun toString() = "JsonAdapter$annotations($type)" } } companion object { operator fun invoke(adapter: Any): AdapterMethodsFactory { val toAdapters = mutableListOf() val fromAdapters = mutableListOf() val classAndSuperclasses = generateSequence(adapter.javaClass) { it.superclass }.iterator() while (classAndSuperclasses.hasNext()) { val clazz = classAndSuperclasses.next() for (declaredMethod in clazz.declaredMethods) { if (declaredMethod.isAnnotationPresent(ToJson::class.java)) { val toAdapter = toAdapter(adapter, declaredMethod) val conflicting = get(toAdapters, toAdapter.type, toAdapter.annotations) checkNull(conflicting) { "Conflicting @ToJson methods:\n ${it.method}\n ${toAdapter.method}" } toAdapters.add(toAdapter) } if (declaredMethod.isAnnotationPresent(FromJson::class.java)) { val fromAdapter = fromAdapter(adapter, declaredMethod) val conflicting = get(fromAdapters, fromAdapter.type, fromAdapter.annotations) checkNull(conflicting) { "Conflicting @FromJson methods:\n ${it.method}\n ${fromAdapter.method}" } fromAdapters.add(fromAdapter) } } } require(toAdapters.isNotEmpty() || fromAdapters.isNotEmpty()) { "Expected at least one @ToJson or @FromJson method on ${adapter.javaClass.name}" } return AdapterMethodsFactory(toAdapters, fromAdapters) } /** * Returns an object that calls a `method` method on `adapter` in service of converting an * object to JSON. */ private fun toAdapter(adapter: Any, method: Method): AdapterMethod { method.isAccessible = true val returnType = method.genericReturnType val parameterTypes = method.genericParameterTypes val parameterAnnotations = method.parameterAnnotations val methodSignatureIncludesJsonWriterAndJsonAdapter = parameterTypes.size >= 2 && parameterTypes[0] == JsonWriter::class.java && returnType == Void.TYPE && parametersAreJsonAdapters(2, parameterTypes) return when { // void pointToJson(JsonWriter jsonWriter, Point point) { // void pointToJson(JsonWriter jsonWriter, Point point, JsonAdapter adapter, ...) { methodSignatureIncludesJsonWriterAndJsonAdapter -> { val qualifierAnnotations = parameterAnnotations[1].jsonAnnotations object : AdapterMethod( adaptersOffset = 2, type = parameterTypes[1], parameterCount = parameterTypes.size, annotations = qualifierAnnotations, adapter = adapter, method = method, nullable = true, ) { override fun toJson(moshi: Moshi, writer: JsonWriter, value: Any?) { invokeMethod(writer, value) } } } parameterTypes.size == 1 && returnType != Void.TYPE -> { // List pointToJson(Point point) { val returnTypeAnnotations = method.jsonAnnotations val qualifierAnnotations = parameterAnnotations[0].jsonAnnotations val nullable = parameterAnnotations[0].hasNullable object : AdapterMethod( adaptersOffset = 1, type = parameterTypes[0], parameterCount = parameterTypes.size, annotations = qualifierAnnotations, adapter = adapter, method = method, nullable = nullable, ) { private lateinit var delegate: JsonAdapter override fun bind(moshi: Moshi, factory: JsonAdapter.Factory) { super.bind(moshi, factory) val shouldSkip = Types.equals(parameterTypes[0], returnType) && qualifierAnnotations == returnTypeAnnotations delegate = if (shouldSkip) { moshi.nextAdapter(factory, returnType, returnTypeAnnotations) } else { moshi.adapter(returnType, returnTypeAnnotations) } } override fun toJson(moshi: Moshi, writer: JsonWriter, value: Any?) { delegate.toJson(writer, invokeMethod(value)) } } } else -> { throw IllegalArgumentException( """ Unexpected signature for $method. @ToJson method signatures may have one of the following structures: void toJson(JsonWriter writer, T value) throws ; void toJson(JsonWriter writer, T value, JsonAdapter delegate, ) throws ; R toJson(T value) throws ; """ .trimIndent() ) } } } /** Returns true if `parameterTypes[offset]` contains only JsonAdapters. */ private fun parametersAreJsonAdapters(offset: Int, parameterTypes: Array): Boolean { for (i in offset until parameterTypes.size) { val parameterType = parameterTypes[i] if (parameterType !is ParameterizedType) return false if (parameterType.rawType != JsonAdapter::class.java) return false } return true } /** * Returns an object that calls a `method` method on `adapter` in service of converting an * object from JSON. */ private fun fromAdapter(adapter: Any, method: Method): AdapterMethod { method.isAccessible = true val returnType = method.genericReturnType val returnTypeAnnotations = method.jsonAnnotations val parameterTypes = method.genericParameterTypes val parameterAnnotations = method.parameterAnnotations val methodSignatureIncludesJsonReaderAndJsonAdapter = parameterTypes.isNotEmpty() && parameterTypes[0] == JsonReader::class.java && returnType != Void.TYPE && parametersAreJsonAdapters(1, parameterTypes) return when { methodSignatureIncludesJsonReaderAndJsonAdapter -> { // Point pointFromJson(JsonReader jsonReader) { // Point pointFromJson(JsonReader jsonReader, JsonAdapter adapter, ...) { object : AdapterMethod( adaptersOffset = 1, type = returnType, parameterCount = parameterTypes.size, annotations = returnTypeAnnotations, adapter = adapter, method = method, nullable = true, ) { override fun fromJson(moshi: Moshi, reader: JsonReader) = invokeMethod(reader) } } parameterTypes.size == 1 && returnType != Void.TYPE -> { // Point pointFromJson(List o) { val qualifierAnnotations = parameterAnnotations[0].jsonAnnotations val nullable = parameterAnnotations[0].hasNullable object : AdapterMethod( adaptersOffset = 1, type = returnType, parameterCount = parameterTypes.size, annotations = returnTypeAnnotations, adapter = adapter, method = method, nullable = nullable, ) { lateinit var delegate: JsonAdapter override fun bind(moshi: Moshi, factory: JsonAdapter.Factory) { super.bind(moshi, factory) delegate = if ( Types.equals(parameterTypes[0], returnType) && qualifierAnnotations == returnTypeAnnotations ) { moshi.nextAdapter(factory, parameterTypes[0], qualifierAnnotations) } else { moshi.adapter(parameterTypes[0], qualifierAnnotations) } } override fun fromJson(moshi: Moshi, reader: JsonReader): Any? { val intermediate = delegate.fromJson(reader) return invokeMethod(intermediate) } } } else -> { throw IllegalArgumentException( """ Unexpected signature for $method. @FromJson method signatures may have one of the following structures: R fromJson(JsonReader jsonReader) throws ; R fromJson(JsonReader jsonReader, JsonAdapter delegate, ) throws ; R fromJson(T value) throws ; """ .trimIndent() ) } } } /** Returns the matching adapter method from the list. */ private fun get( adapterMethods: List, type: Type, annotations: Set, ): AdapterMethod? { for (adapterMethod in adapterMethods) { if (Types.equals(adapterMethod.type, type) && adapterMethod.annotations == annotations) { return adapterMethod } } return null } } internal abstract class AdapterMethod( private val adaptersOffset: Int, type: Type, parameterCount: Int, val annotations: Set, val adapter: Any, val method: Method, val nullable: Boolean, ) { val type = type.canonicalize() private val jsonAdapters: Array?> = arrayOfNulls(parameterCount - adaptersOffset) open fun bind(moshi: Moshi, factory: JsonAdapter.Factory) { if (jsonAdapters.isNotEmpty()) { val parameterTypes = method.genericParameterTypes val parameterAnnotations = method.parameterAnnotations for (i in adaptersOffset until parameterTypes.size) { val type = (parameterTypes[i] as ParameterizedType).actualTypeArguments[0] val jsonAnnotations = parameterAnnotations[i].jsonAnnotations jsonAdapters[i - adaptersOffset] = if (Types.equals(this.type, type) && annotations == jsonAnnotations) { moshi.nextAdapter(factory, type, jsonAnnotations) } else { moshi.adapter(type, jsonAnnotations) } } } } open fun toJson(moshi: Moshi, writer: JsonWriter, value: Any?): Unit = throw AssertionError() open fun fromJson(moshi: Moshi, reader: JsonReader): Any? = throw AssertionError() /** Invoke the method with one fixed argument, plus any number of JSON adapter arguments. */ protected fun invokeMethod(arg: Any?): Any? { val args = arrayOfNulls(1 + jsonAdapters.size) args[0] = arg jsonAdapters.copyInto(args, 1, 0, jsonAdapters.size) return try { method.invoke(adapter, *args) } catch (e: IllegalAccessException) { throw AssertionError() } } /** Invoke the method with two fixed arguments, plus any number of JSON adapter arguments. */ protected fun invokeMethod(arg0: Any?, arg1: Any?): Any? { val args = arrayOfNulls(2 + jsonAdapters.size) args[0] = arg0 args[1] = arg1 jsonAdapters.copyInto(args, 2, 0, jsonAdapters.size) return try { method.invoke(adapter, *args) } catch (e: IllegalAccessException) { throw AssertionError() } } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/ArrayJsonAdapter.kt ================================================ /* * 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 * * https://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.squareup.moshi.internal import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.rawType import java.lang.reflect.Array as JavaArray import java.lang.reflect.Type /** * Converts arrays to JSON arrays containing their converted contents. This supports both primitive * and object arrays. */ internal class ArrayJsonAdapter( private val elementClass: Class<*>, private val elementAdapter: JsonAdapter, ) : JsonAdapter() { override fun fromJson(reader: JsonReader): Any { val list = buildList { reader.beginArray() while (reader.hasNext()) { add(elementAdapter.fromJson(reader)) } reader.endArray() } val array = JavaArray.newInstance(elementClass, list.size) list.forEachIndexed { i, item -> JavaArray.set(array, i, item) } return array } override fun toJson(writer: JsonWriter, value: Any?) { writer.beginArray() when (value) { is BooleanArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } is ByteArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } is CharArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } is DoubleArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } is FloatArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } is IntArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } is LongArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } is ShortArray -> { for (element in value) { elementAdapter.toJson(writer, element) } } is Array<*> -> { for (element in value) { elementAdapter.toJson(writer, element) } } } writer.endArray() } override fun toString() = "$elementAdapter.array()" companion object Factory : JsonAdapter.Factory { override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { val elementType = arrayComponentType(type) ?: return null if (annotations.isNotEmpty()) return null val elementClass = elementType.rawType val elementAdapter = moshi.adapter(elementType) return ArrayJsonAdapter(elementClass, elementAdapter).nullSafe() } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/ClassFactory.kt ================================================ /* * Copyright (C) 2011 Google 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.squareup.moshi.internal import java.io.ObjectInputStream import java.io.ObjectStreamClass import java.lang.reflect.InvocationTargetException /** * Magic that creates instances of arbitrary concrete classes. Derived from Gson's UnsafeAllocator * and ConstructorConstructor classes. * * @author Joel Leitch * @author Jesse Wilson */ internal abstract class ClassFactory { abstract fun newInstance(): T companion object { @JvmStatic fun get(rawType: Class<*>): ClassFactory { // Try to find a no-args constructor. May be any visibility including private. try { val constructor = rawType.getDeclaredConstructor() constructor.isAccessible = true return object : ClassFactory() { @Suppress("UNCHECKED_CAST") override fun newInstance() = constructor.newInstance() as T override fun toString() = rawType.name } } catch (ignored: NoSuchMethodException) { // No no-args constructor. Fall back to something more magical... } // Try the JVM's Unsafe mechanism. // public class Unsafe { // public Object allocateInstance(Class type); // } try { val unsafeClass = Class.forName("sun.misc.Unsafe") val unsafeField = unsafeClass.getDeclaredField("theUnsafe") unsafeField.isAccessible = true val unsafe = unsafeField[null] val allocateInstance = unsafeClass.getMethod("allocateInstance", Class::class.java) return object : ClassFactory() { @Suppress("UNCHECKED_CAST") override fun newInstance() = allocateInstance.invoke(unsafe, rawType) as T override fun toString() = rawType.name } } catch (e: IllegalAccessException) { throw AssertionError() } catch (ignored: ClassNotFoundException) { // Not the expected version of the Oracle Java library! } catch (ignored: NoSuchMethodException) { // Not the expected version of the Oracle Java library! } catch (ignored: NoSuchFieldException) { // Not the expected version of the Oracle Java library! } // Try (post-Gingerbread) Dalvik/libcore's ObjectStreamClass mechanism. // public class ObjectStreamClass { // private static native int getConstructorId(Class c); // private static native Object newInstance(Class instantiationClass, int methodId); // } try { val getConstructorId = ObjectStreamClass::class.java.getDeclaredMethod("getConstructorId", Class::class.java) getConstructorId.isAccessible = true val constructorId = getConstructorId.invoke(null, Any::class.java) as Int val newInstance = ObjectStreamClass::class .java .getDeclaredMethod("newInstance", Class::class.java, Int::class.javaPrimitiveType) newInstance.isAccessible = true return object : ClassFactory() { @Suppress("UNCHECKED_CAST") override fun newInstance() = newInstance.invoke(null, rawType, constructorId) as T override fun toString() = rawType.name } } catch (e: IllegalAccessException) { throw AssertionError() } catch (e: InvocationTargetException) { throw e.rethrowCause() } catch (ignored: NoSuchMethodException) { // Not the expected version of Dalvik/libcore! } // Try (pre-Gingerbread) Dalvik/libcore's ObjectInputStream mechanism. // public class ObjectInputStream { // private static native Object newInstance( // Class instantiationClass, Class constructorClass); // } try { val newInstance = ObjectInputStream::class .java .getDeclaredMethod("newInstance", Class::class.java, Class::class.java) newInstance.isAccessible = true return object : ClassFactory() { @Suppress("UNCHECKED_CAST") override fun newInstance() = newInstance.invoke(null, rawType, Any::class.java) as T override fun toString() = rawType.name } } catch (ignored: Exception) {} throw IllegalArgumentException("cannot construct instances of ${rawType.name}") } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/ClassJsonAdapter.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.internal import com.squareup.moshi.Json import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.rawType import java.lang.reflect.Field import java.lang.reflect.InvocationTargetException import java.lang.reflect.Modifier.isAbstract import java.lang.reflect.Modifier.isProtected import java.lang.reflect.Modifier.isPublic import java.lang.reflect.Modifier.isStatic import java.lang.reflect.Modifier.isTransient import java.lang.reflect.ParameterizedType import java.lang.reflect.Type /** * Emits a regular class as a JSON object by mapping Java fields to JSON object properties. * * # Platform Types * * Fields from platform classes are omitted from both serialization and deserialization unless they * are either public or protected. This includes the following packages and their subpackages: * * `android.*` * * `androidx.*` * * `java.*` * * `javax.*` * * `kotlin.*` * * `kotlinx.*` * * `scala.*` */ internal class ClassJsonAdapter( private val classFactory: ClassFactory, fieldsMap: Map>, ) : JsonAdapter() { private val fieldsArray = fieldsMap.values.toTypedArray() private val options = JsonReader.Options.of(*fieldsMap.keys.toTypedArray()) override fun fromJson(reader: JsonReader): T { val result: T = try { classFactory.newInstance() } catch (e: InstantiationException) { throw RuntimeException(e) } catch (e: InvocationTargetException) { throw e.rethrowCause() } catch (_: IllegalAccessException) { throw AssertionError() } try { reader.beginObject() while (reader.hasNext()) { val index = reader.selectName(options) if (index == -1) { reader.skipName() reader.skipValue() continue } fieldsArray[index].read(reader, result) } reader.endObject() return result } catch (_: IllegalAccessException) { throw AssertionError() } } override fun toJson(writer: JsonWriter, value: T?) { try { writer.beginObject() for (fieldBinding in fieldsArray) { writer.name(fieldBinding.name) fieldBinding.write(writer, value) } writer.endObject() } catch (_: IllegalAccessException) { throw AssertionError() } } override fun toString() = "JsonAdapter($classFactory)" internal class FieldBinding(val name: String, val field: Field, val adapter: JsonAdapter) { fun read(reader: JsonReader, value: Any?) { val fieldValue = adapter.fromJson(reader) field.set(value, fieldValue) } @Suppress("UNCHECKED_CAST") // We require that field's values are of type T. fun write(writer: JsonWriter, value: Any?) { val fieldValue = field.get(value) as T adapter.toJson(writer, fieldValue) } } companion object Factory : JsonAdapter.Factory { override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (type !is Class<*> && type !is ParameterizedType) { return null } val rawType = type.rawType if (rawType.isInterface || rawType.isEnum) return null if (annotations.isNotEmpty()) return null if (rawType.isPlatformType) { type.throwIfIsCollectionClass(List::class.java) type.throwIfIsCollectionClass(Set::class.java) type.throwIfIsCollectionClass(Map::class.java) type.throwIfIsCollectionClass(Collection::class.java) val messagePrefix = buildString { append("Platform $rawType") if (type is ParameterizedType) { append(" in $type") } } throw IllegalArgumentException( "$messagePrefix requires explicit JsonAdapter to be registered" ) } require(!rawType.isAnonymousClass) { "Cannot serialize anonymous class ${rawType.name}" } require(!rawType.isLocalClass) { "Cannot serialize local class ${rawType.name}" } val isNonStaticNestedClass = rawType.enclosingClass != null && !isStatic(rawType.modifiers) require(!isNonStaticNestedClass) { "Cannot serialize non-static nested class ${rawType.name}" } require(!isAbstract(rawType.modifiers)) { "Cannot serialize abstract class ${rawType.name}" } require(!rawType.isKotlin) { "Cannot serialize Kotlin type ${rawType.name}. Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapterFactory from the moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact." } val classFactory = ClassFactory.get(rawType) val fields = sortedMapOf>() var parentType = type while (parentType != Any::class.java) { createFieldBindings(moshi, parentType, fields) parentType = getGenericSuperclass(parentType) } return ClassJsonAdapter(classFactory, fields).nullSafe() } /** * Throw clear error messages for the common beginner mistake of using the concrete collection * classes instead of the collection interfaces, eg: ArrayList instead of List. */ private fun Type.throwIfIsCollectionClass(collectionInterface: Class<*>) { require(!collectionInterface.isAssignableFrom(rawType)) { "No JsonAdapter for $this, you should probably use ${collectionInterface.simpleName} instead of ${rawType.simpleName} (Moshi only supports the collection interfaces by default) or else register a custom JsonAdapter." } } /** Creates a field binding for each of declared field of `type`. */ private fun createFieldBindings( moshi: Moshi, type: Type, fieldBindings: MutableMap>, ) { val rawType = type.rawType val platformType = rawType.isPlatformType for (field in rawType.declaredFields) { if (!includeField(platformType, field.modifiers)) continue val jsonAnnotation = field.getAnnotation(Json::class.java) if (jsonAnnotation != null && jsonAnnotation.ignore) continue // Look up a type adapter for this type. val fieldType = field.genericType.resolve(type, rawType) val annotations = field.jsonAnnotations val fieldName = field.name val adapter = moshi.adapter(type = fieldType, annotations = annotations, fieldName = fieldName) // Create the binding between field and JSON. field.isAccessible = true // Store it using the field's name. If there was already a field with this name, fail! val jsonName = jsonAnnotation.jsonName(fieldName) val fieldBinding = FieldBinding(jsonName, field, adapter) val replaced = fieldBindings.put(jsonName, fieldBinding) checkNull(replaced) { "Conflicting fields:\n ${it.field}\n ${fieldBinding.field}" } } } /** Returns true if fields with `modifiers` are included in the emitted JSON. */ private fun includeField(platformType: Boolean, modifiers: Int): Boolean { return if (isStatic(modifiers) || isTransient(modifiers)) { false } else { isPublic(modifiers) || isProtected(modifiers) || !platformType } } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/CollectionJsonAdapter.kt ================================================ /* * 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 * * https://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.squareup.moshi.internal 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 com.squareup.moshi.rawType import java.lang.reflect.Type /** Converts collection types to JSON arrays containing their converted contents. */ internal abstract class CollectionJsonAdapter, T> private constructor(private val elementAdapter: JsonAdapter) : JsonAdapter() { abstract fun newCollection(): C override fun fromJson(reader: JsonReader): C { val result = newCollection() reader.beginArray() while (reader.hasNext()) { result.add(elementAdapter.fromJson(reader)) } reader.endArray() return result } override fun toJson(writer: JsonWriter, value: C?) { markNotNull(value) // Always wrapped in nullSafe() writer.beginArray() for (element in value) { elementAdapter.toJson(writer, element) } writer.endArray() } override fun toString() = "$elementAdapter.collection()" companion object Factory : JsonAdapter.Factory { override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (annotations.isNotEmpty()) return null return when (type.rawType) { List::class.java, Collection::class.java -> { newArrayListAdapter(type, moshi).nullSafe() } Set::class.java -> { newLinkedHashSetAdapter(type, moshi).nullSafe() } else -> null } } private fun newArrayListAdapter( type: Type, moshi: Moshi, ): JsonAdapter?> { val elementType = Types.collectionElementType(type, Collection::class.java) val elementAdapter = moshi.adapter(elementType) return object : CollectionJsonAdapter, T>(elementAdapter) { override fun newCollection(): MutableCollection = ArrayList() } } private fun newLinkedHashSetAdapter( type: Type, moshi: Moshi, ): JsonAdapter?> { val elementType = Types.collectionElementType(type, Collection::class.java) val elementAdapter = moshi.adapter(elementType) return object : CollectionJsonAdapter, T>(elementAdapter) { override fun newCollection(): MutableSet = LinkedHashSet() } } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/JsonScope.kt ================================================ /* * Copyright (C) 2010 Google 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.squareup.moshi.internal /** Lexical scoping elements within a JSON reader or writer. */ internal object JsonScope { /** An array with no elements requires no separators or newlines before it is closed. */ const val EMPTY_ARRAY = 1 /** An array with at least one value requires a comma and newline before the next element. */ const val NONEMPTY_ARRAY = 2 /** An object with no name/value pairs requires no separators or newlines before it is closed. */ const val EMPTY_OBJECT = 3 /** An object whose most recent element is a key. The next element must be a value. */ const val DANGLING_NAME = 4 /** An object with at least one name/value pair requires a separator before the next element. */ const val NONEMPTY_OBJECT = 5 /** No object or array has been started. */ const val EMPTY_DOCUMENT = 6 /** A document with at an array or object. */ const val NONEMPTY_DOCUMENT = 7 /** A document that's been closed and cannot be accessed. */ const val CLOSED = 8 /** Sits above the actual state to indicate that a value is currently being streamed in. */ const val STREAMING_VALUE = 9 /** * Renders the path in a JSON document to a string. The `pathNames` and `pathIndices` parameters * corresponds directly to stack: At indices where the stack contains an object (EMPTY_OBJECT, * DANGLING_NAME or NONEMPTY_OBJECT), pathNames contains the name at this scope. Where it contains * an array (EMPTY_ARRAY, NONEMPTY_ARRAY) pathIndices contains the current index in that array. * Otherwise the value is undefined, and we take advantage of that by incrementing pathIndices * when doing so isn't useful. */ @JvmStatic fun getPath( stackSize: Int, stack: IntArray, pathNames: Array, pathIndices: IntArray, ): String { return buildString { append('$') for (i in 0 until stackSize) { when (stack[i]) { EMPTY_ARRAY, NONEMPTY_ARRAY -> append('[').append(pathIndices[i]).append(']') EMPTY_OBJECT, DANGLING_NAME, NONEMPTY_OBJECT -> { append('.') if (pathNames[i] != null) { append(pathNames[i]) } } NONEMPTY_DOCUMENT, EMPTY_DOCUMENT, CLOSED -> {} } } } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/JsonValueSource.kt ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi.internal import com.squareup.moshi.JsonReader import kotlin.math.min import okio.Buffer import okio.BufferedSource import okio.ByteString import okio.ByteString.Companion.EMPTY import okio.ByteString.Companion.encodeUtf8 import okio.EOFException import okio.Source /** * This source reads a prefix of another source as a JSON value and then terminates. It can read * top-level arrays, objects, or strings only. * * It implements [lenient parsing][JsonReader.setLenient] and has no mechanism to enforce strict * parsing. If the input is not valid or lenient JSON the behavior of this source is unspecified. */ internal class JsonValueSource @JvmOverloads constructor( private val source: BufferedSource, /** If non-empty, data from this should be returned before data from [source]. */ private val prefix: Buffer = Buffer(), /** * The state indicates what kind of data is readable at [limit]. This also serves double-duty as * the type of bytes we're interested in while in this state. */ private var state: ByteString = STATE_JSON, /** * The level of nesting of arrays and objects. When the end of string, array, or object is * reached, this should be compared against 0. If it is zero, then we've read a complete value and * this source is exhausted. */ private var stackSize: Int = 0, ) : Source { private val buffer: Buffer = source.buffer /** The number of bytes immediately returnable to the caller. */ private var limit: Long = 0 private var closed = false /** * Advance [limit] until any of these conditions are met: * * Limit is at least `byteCount`. We can satisfy the caller's request! * * The JSON value is complete. This stream is exhausted. * * We have some data to return and returning more would require reloading the buffer. We prefer * to return some data immediately when more data requires blocking. * * @throws EOFException if the stream is exhausted before the JSON object completes. */ private fun advanceLimit(byteCount: Long) { while (limit < byteCount) { // If we've finished the JSON object, we're done. if (state === STATE_END_OF_JSON) { return } // If we can't return any bytes without more data in the buffer, grow the buffer. if (limit == buffer.size) { if (limit > 0L) return source.require(1L) } // Find the next interesting character for the current state. If the buffer doesn't have one, // then we can read the entire buffer. val index = buffer.indexOfElement(state, limit) if (index == -1L) { limit = buffer.size continue } val b = buffer[index] when { state === STATE_JSON -> when (b.toInt().toChar()) { '[', '{' -> { stackSize++ limit = index + 1 } ']', '}' -> { stackSize-- if (stackSize == 0) state = STATE_END_OF_JSON limit = index + 1 } '\"' -> { state = STATE_DOUBLE_QUOTED limit = index + 1 } '\'' -> { state = STATE_SINGLE_QUOTED limit = index + 1 } '/' -> { source.require(index + 2) when (buffer[index + 1]) { '/'.code.toByte() -> { state = STATE_END_OF_LINE_COMMENT limit = index + 2 } '*'.code.toByte() -> { state = STATE_C_STYLE_COMMENT limit = index + 2 } else -> { limit = index + 1 } } } '#' -> { state = STATE_END_OF_LINE_COMMENT limit = index + 1 } } state === STATE_SINGLE_QUOTED || state === STATE_DOUBLE_QUOTED -> { if (b == '\\'.code.toByte()) { source.require(index + 2) limit = index + 2 } else { state = if (stackSize > 0) STATE_JSON else STATE_END_OF_JSON limit = index + 1 } } state === STATE_C_STYLE_COMMENT -> { source.require(index + 2) if (buffer[index + 1] == '/'.code.toByte()) { limit = index + 2 state = STATE_JSON } else { limit = index + 1 } } state === STATE_END_OF_LINE_COMMENT -> { limit = index + 1 state = STATE_JSON } else -> { throw AssertionError() } } } } /** * Discards any remaining JSON data in this source that was left behind after it was closed. It is * an error to call [read] after calling this method. */ fun discard() { closed = true while (state !== STATE_END_OF_JSON) { advanceLimit(8192) source.skip(limit) } } override fun read(sink: Buffer, byteCount: Long): Long { var mutableByteCount = byteCount check(!closed) { "closed" } if (mutableByteCount == 0L) return 0L // If this stream has a prefix, consume that first. if (!prefix.exhausted()) { val prefixResult = prefix.read(sink, mutableByteCount) mutableByteCount -= prefixResult if (buffer.exhausted()) return prefixResult // Defer a blocking call. val suffixResult = read(sink, mutableByteCount) return if (suffixResult != -1L) suffixResult + prefixResult else prefixResult } advanceLimit(mutableByteCount) if (limit == 0L) { if (state !== STATE_END_OF_JSON) throw AssertionError() return -1L } val result = min(mutableByteCount, limit) sink.write(buffer, result) limit -= result return result } override fun timeout() = source.timeout() override fun close() { // Note that this does not close the underlying source; that's the creator's responsibility. closed = true } companion object { val STATE_JSON: ByteString = "[]{}\"'/#".encodeUtf8() val STATE_SINGLE_QUOTED: ByteString = "'\\".encodeUtf8() val STATE_DOUBLE_QUOTED: ByteString = "\"\\".encodeUtf8() val STATE_END_OF_LINE_COMMENT: ByteString = "\r\n".encodeUtf8() val STATE_C_STYLE_COMMENT: ByteString = "*".encodeUtf8() val STATE_END_OF_JSON: ByteString = EMPTY } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/KotlinReflectTypes.kt ================================================ /* * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. */ package com.squareup.moshi.internal import java.lang.reflect.GenericDeclaration import java.lang.reflect.Modifier import java.lang.reflect.Type import java.lang.reflect.TypeVariable import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.KTypeParameter import kotlin.reflect.KTypeProjection import kotlin.reflect.KVariance /* * Moshi wants to offer something like `kotlin.reflect.KType.javaType` as a stable API, but that * function is `@ExperimentalStdlibApi`. * * This file contains a copy-paste of that code, implemented on only non-experimental APIs. It's * also modified to use Moshi's `Type` implementations. * * If/when Kotlin offers a non-experimental API to convert a `KType` to a Java `Type`, we should * migrate to that and delete this file. */ @SinceKotlin("1.4") internal val KType.javaType: Type get() = computeJavaType() private fun KType.computeJavaType(forceWrapper: Boolean = false): Type { when (val classifier = classifier) { is KTypeParameter -> return TypeVariableImpl(classifier) is KClass<*> -> { val jClass = if (forceWrapper) classifier.javaObjectType else classifier.java val arguments = arguments if (arguments.isEmpty()) return jClass if (jClass.isArray) { if (jClass.componentType.isPrimitive) return jClass val (variance, elementType) = arguments.singleOrNull() ?: throw IllegalArgumentException( "kotlin.Array must have exactly one type argument: $this" ) return when (variance) { // Array is always erased to Object[], and Array<*> is Object[]. null, KVariance.IN -> jClass KVariance.INVARIANT, KVariance.OUT -> { val javaElementType = elementType!!.computeJavaType() if (javaElementType is Class<*>) jClass else GenericArrayTypeImpl(javaElementType) } } } return createPossiblyInnerType(jClass, arguments) } else -> throw UnsupportedOperationException("Unsupported type classifier: $this") } } private fun createPossiblyInnerType(jClass: Class<*>, arguments: List): Type { val ownerClass = jClass.declaringClass ?: return ParameterizedTypeImpl( ownerType = null, rawType = jClass, typeArguments = arguments.map(KTypeProjection::javaType).toTypedArray(), ) if (Modifier.isStatic(jClass.modifiers)) { return ParameterizedTypeImpl( ownerType = ownerClass, rawType = jClass, typeArguments = arguments.map(KTypeProjection::javaType).toTypedArray(), ) } val n = jClass.typeParameters.size return ParameterizedTypeImpl( ownerType = createPossiblyInnerType(ownerClass, arguments.subList(n, arguments.size)), rawType = jClass, typeArguments = arguments.subList(0, n).map(KTypeProjection::javaType).toTypedArray(), ) } private val KTypeProjection.javaType: Type get() { val variance = variance ?: return WildcardTypeImpl(upperBound = Any::class.java, lowerBound = null) val type = type!! // TODO: JvmSuppressWildcards return when (variance) { KVariance.INVARIANT -> { // TODO: declaration-site variance type.computeJavaType(forceWrapper = true) } KVariance.IN -> WildcardTypeImpl( upperBound = Any::class.java, lowerBound = type.computeJavaType(forceWrapper = true), ) KVariance.OUT -> WildcardTypeImpl(upperBound = type.computeJavaType(forceWrapper = true), lowerBound = null) } } // Suppression of the error is needed for `AnnotatedType[] getAnnotatedBounds()` which is impossible // to implement on JDK 6 // because `AnnotatedType` has only appeared in JDK 8. @Suppress("ABSTRACT_MEMBER_NOT_IMPLEMENTED") private class TypeVariableImpl(private val typeParameter: KTypeParameter) : TypeVariable { override fun getName(): String = typeParameter.name override fun getGenericDeclaration(): GenericDeclaration = TODO( "getGenericDeclaration() is not yet supported for type variables created from KType: $typeParameter" ) override fun getBounds(): Array = typeParameter.upperBounds.map { it.computeJavaType(forceWrapper = true) }.toTypedArray() override fun equals(other: Any?): Boolean = other is TypeVariable<*> && name == other.name && genericDeclaration == other.genericDeclaration override fun hashCode(): Int = name.hashCode() xor genericDeclaration.hashCode() override fun toString(): String = name } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/LinkedHashTreeMap.kt ================================================ package com.squareup.moshi.internal import com.squareup.moshi.internal.LinkedHashTreeMap.Node import java.io.Serializable import kotlin.math.max @Suppress("UNCHECKED_CAST") private val NATURAL_ORDER = Comparator { o1, o2 -> (o1 as Comparable).compareTo(o2) } /** * A map of comparable keys to values. Unlike TreeMap, this class uses insertion order for iteration * order. Comparison order is only used as an optimization for efficient insertion and removal. * * This implementation was derived from Android 4.1's TreeMap and LinkedHashMap classes. * * @param comparator the comparator to order elements with, or null to use the natural ordering. */ internal class LinkedHashTreeMap(comparator: Comparator? = null) : AbstractMutableMap(), Serializable { @Suppress("UNCHECKED_CAST") private val comparator: Comparator = (comparator ?: NATURAL_ORDER) as Comparator private var table: Array?> = arrayOfNulls(16) // TODO: sizing/resizing policies private val header: Node = Node() override var size = 0 private var modCount = 0 private var threshold = table.size / 2 + table.size / 4 // 3/4 capacity private var entrySet: EntrySet? = null private var keySet: KeySet? = null override val keys: MutableSet get() = keySet ?: KeySet().also { keySet = it } override fun put(key: K, value: V): V? { val created = findOrCreate(key) val result = created.value created.mutableValue = value return result } override val entries: MutableSet> get() = entrySet ?: EntrySet().also { entrySet = it } override fun get(key: K) = findByObject(key)?.value override fun containsKey(key: K) = findByObject(key) != null override fun clear() { table.fill(null) size = 0 modCount++ // Clear all links to help GC val header = header var e = header.next!! while (e !== header) { val next = e.next e.prev = null e.next = null e = next!! } header.prev = header header.next = header } override fun remove(key: K) = removeInternalByKey(key)?.value class Node : MutableMap.MutableEntry { @JvmField var parent: Node? = null @JvmField var left: Node? = null @JvmField var right: Node? = null @JvmField var next: Node? @JvmField var prev: Node? private var realKey: K? = null override val key: K get() = knownNotNull(realKey) @JvmField val hash: Int @JvmField var mutableValue: V? = null override val value: V? get() = mutableValue @JvmField var height = 0 /** Create the header entry. */ constructor() { realKey = null hash = -1 prev = this next = prev } /** Create a regular entry. */ constructor(parent: Node?, key: K, hash: Int, next: Node, prev: Node) { this.parent = parent this.realKey = key this.hash = hash height = 1 this.next = next this.prev = prev prev.next = this next.prev = this } override fun setValue(newValue: V?): V? { val oldValue = this.value this.mutableValue = newValue return oldValue } override fun equals(other: Any?): Boolean { if (other is Map.Entry<*, *>) { val (key1, value1) = other return ((if (realKey == null) key1 == null else realKey == key1) && if (value == null) value1 == null else value == value1) } return false } override fun hashCode(): Int { return (realKey?.hashCode() ?: 0) xor (value?.hashCode() ?: 0) } override fun toString() = "$key=$value" /** Returns the first node in this subtree. */ fun first(): Node { var node = this var child = node.left while (child != null) { node = child child = node.left } return node } /** Returns the last node in this subtree. */ fun last(): Node { var node = this var child = node.right while (child != null) { node = child child = node.right } return node } } private fun doubleCapacity() { table = doubleCapacity(table) threshold = table.size / 2 + table.size / 4 // 3/4 capacity } /** * Returns the node at or adjacent to the given key, creating it if requested. * * @throws ClassCastException if `key` and the tree's keys aren't mutually comparable. */ private fun findOrCreate(key: K): Node { return knownNotNull(find(key, create = true)) } /** * Returns the node at or adjacent to the given key, creating it if requested. * * @throws ClassCastException if `key` and the tree's keys aren't mutually comparable. */ fun find(key: K, create: Boolean): Node? { val comparator: Comparator = comparator val table = table val hash = secondaryHash(key.hashCode()) val index = hash and (table.size - 1) var nearest = table[index] var comparison = 0 if (nearest != null) { // Micro-optimization: avoid polymorphic calls to Comparator.compare(). // Throws a ClassCastException below if there's trouble. @Suppress("UNCHECKED_CAST") val comparableKey = if (comparator === NATURAL_ORDER) key as Comparable else null while (true) { comparison = comparableKey?.compareTo(knownNotNull(nearest).key) ?: comparator.compare(key, knownNotNull(nearest).key) // We found the requested key. if (comparison == 0) { return nearest } // If it exists, the key is in a subtree. Go deeper. val child = (if (comparison < 0) knownNotNull(nearest).left else knownNotNull(nearest).right) ?: break nearest = child } } // The key doesn't exist in this tree. if (!create) { return null } // Create the node and add it to the tree or the table. val header = header val created: Node if (nearest == null) { // Check that the value is comparable if we didn't do any comparisons. if (comparator === NATURAL_ORDER && key !is Comparable<*>) { throw ClassCastException("${(key as Any).javaClass.name} is not Comparable") } created = Node(null, key, hash, header, knownNotNull(header.prev)) table[index] = created } else { created = Node(nearest, key, hash, header, knownNotNull(header.prev)) if (comparison < 0) { // nearest.key is higher nearest.left = created } else { // comparison > 0, nearest.key is lower nearest.right = created } rebalance(nearest, true) } if (size++ > threshold) { doubleCapacity() } modCount++ return created } private fun findByObject(key: Any?): Node? { return try { @Suppress("UNCHECKED_CAST") if (key != null) find(key as K, false) else null } catch (_: ClassCastException) { null } } /** * Returns this map's entry that has the same key and value as `entry`, or null if this map has no * such entry. * * This method uses the comparator for key equality rather than `equals`. If this map's comparator * isn't consistent with equals (such as `String.CASE_INSENSITIVE_ORDER`), then `remove()` and * `contains()` will violate the collections API. */ fun findByEntry(entry: Map.Entry<*, *>): Node? { val mine = findByObject(entry.key) val valuesEqual = mine != null && equal(mine.value, entry.value) return if (valuesEqual) mine else null } private fun equal(a: Any?, b: Any?): Boolean { @Suppress("SuspiciousEqualsCombination") return (a === b) || ((a != null) && (a == b)) } /** * Applies a supplemental hash function to a given hashCode, which defends against poor quality * hash functions. This is critical because HashMap uses power-of-two length hash tables, that * otherwise encounter collisions for hashCodes that do not differ in lower or upper bits. */ private fun secondaryHash(seed: Int): Int { // Doug Lea's supplemental hash function var h = seed h = h xor (h ushr 20 xor (h ushr 12)) return h xor (h ushr 7) xor (h ushr 4) } /** * Removes `node` from this tree, rearranging the tree's structure as necessary. * * @param unlink true to also unlink this node from the iteration linked list. */ fun removeInternal(node: Node, unlink: Boolean) { if (unlink) { knownNotNull(node.prev).next = node.next knownNotNull(node.next).prev = node.prev node.prev = null node.next = null // Help the GC (for performance) } var left = node.left var right = node.right val originalParent = node.parent if (left != null && right != null) { /* * To remove a node with both left and right subtrees, move an * adjacent node from one of those subtrees into this node's place. * * Removing the adjacent node may change this node's subtrees. This * node may no longer have two subtrees once the adjacent node is * gone! */ val adjacent = if (left.height > right.height) left.last() else right.first() removeInternal(adjacent, false) // takes care of rebalance and size-- var leftHeight = 0 left = node.left if (left != null) { leftHeight = left.height adjacent.left = left left.parent = adjacent node.left = null } var rightHeight = 0 right = node.right if (right != null) { rightHeight = right.height adjacent.right = right right.parent = adjacent node.right = null } adjacent.height = max(leftHeight, rightHeight) + 1 replaceInParent(node, adjacent) return } else if (left != null) { replaceInParent(node, left) node.left = null } else if (right != null) { replaceInParent(node, right) node.right = null } else { replaceInParent(node, null) } rebalance(originalParent, false) size-- modCount++ } fun removeInternalByKey(key: Any?): Node? { val node = findByObject(key) if (node != null) { removeInternal(node, true) } return node } private fun replaceInParent(node: Node, replacement: Node?) { val parent = node.parent node.parent = null if (replacement != null) { replacement.parent = parent } if (parent != null) { if (parent.left === node) { parent.left = replacement } else { assert(parent.right === node) parent.right = replacement } } else { val index = node.hash and (table.size - 1) table[index] = replacement } } /** * Rebalances the tree by making any AVL rotations necessary between the newly-unbalanced node and * the tree's root. * * @param insert true if the node was unbalanced by an insert; false if it was by a removal. */ private fun rebalance(unbalanced: Node?, insert: Boolean) { var node = unbalanced while (node != null) { val left = node.left val right = node.right val leftHeight = left?.height ?: 0 val rightHeight = right?.height ?: 0 val delta = leftHeight - rightHeight when (delta) { -2 -> { val rightLeft = right!!.left val rightRight = right.right val rightRightHeight = rightRight?.height ?: 0 val rightLeftHeight = rightLeft?.height ?: 0 val rightDelta = rightLeftHeight - rightRightHeight if (rightDelta != -1 && (rightDelta != 0 || insert)) { assert(rightDelta == 1) rotateRight(right) // AVL right left } rotateLeft(node) // AVL right right if (insert) { break // no further rotations will be necessary } } 2 -> { val leftLeft = left!!.left val leftRight = left.right val leftRightHeight = leftRight?.height ?: 0 val leftLeftHeight = leftLeft?.height ?: 0 val leftDelta = leftLeftHeight - leftRightHeight if (leftDelta != 1 && (leftDelta != 0 || insert)) { assert(leftDelta == -1) rotateLeft(left) // AVL left right } rotateRight(node) // AVL left left if (insert) { break // no further rotations will be necessary } } 0 -> { node.height = leftHeight + 1 // leftHeight == rightHeight if (insert) { break // the insert caused balance, so rebalancing is done! } } else -> { assert(delta == -1 || delta == 1) node.height = max(leftHeight, rightHeight) + 1 if (!insert) { break // the height hasn't changed, so rebalancing is done! } } } node = node.parent } } /** Rotates the subtree so that its root's right child is the new root. */ private fun rotateLeft(root: Node) { val left = root.left val pivot = root.right!! val pivotLeft = pivot.left val pivotRight = pivot.right // move the pivot's left child to the root's right root.right = pivotLeft if (pivotLeft != null) { pivotLeft.parent = root } replaceInParent(root, pivot) // move the root to the pivot's left pivot.left = root root.parent = pivot // fix heights root.height = max(left?.height ?: 0, pivotLeft?.height ?: 0) + 1 pivot.height = max(root.height, pivotRight?.height ?: 0) + 1 } /** Rotates the subtree so that its root's left child is the new root. */ private fun rotateRight(root: Node) { val pivot = root.left!! val right = root.right val pivotLeft = pivot.left val pivotRight = pivot.right // move the pivot's right child to the root's left root.left = pivotRight if (pivotRight != null) { pivotRight.parent = root } replaceInParent(root, pivot) // move the root to the pivot's right pivot.right = root root.parent = pivot // fixup heights root.height = max(right?.height ?: 0, pivotRight?.height ?: 0) + 1 pivot.height = max(root.height, pivotLeft?.height ?: 0) + 1 } abstract inner class LinkedTreeMapIterator : MutableIterator { var next: Node = header.next!! private var lastReturned: Node? = null private var expectedModCount: Int = modCount override fun hasNext(): Boolean = next !== header fun nextNode(): Node { val e = next if (e === header) { throw NoSuchElementException() } if (modCount != expectedModCount) { throw ConcurrentModificationException() } next = e.next!! return e.also { lastReturned = it } } override fun remove() { removeInternal(checkNotNull(lastReturned), true) lastReturned = null expectedModCount = modCount } } inner class EntrySet : AbstractMutableSet>() { override val size: Int get() = this@LinkedHashTreeMap.size override fun iterator(): MutableIterator> { return object : LinkedTreeMapIterator>() { override fun next(): MutableMap.MutableEntry { @Suppress("UNCHECKED_CAST") return nextNode() as MutableMap.MutableEntry } } } override fun contains(element: MutableMap.MutableEntry): Boolean { return findByEntry(element) != null } override fun remove(element: MutableMap.MutableEntry): Boolean { if (element !is Node<*, *>) { return false } val node: Node = findByEntry(element) ?: return false removeInternal(node, true) return true } override fun clear() { this@LinkedHashTreeMap.clear() } override fun add(element: MutableMap.MutableEntry): Boolean { throw NotImplementedError() } } inner class KeySet : AbstractMutableSet() { override val size: Int get() = this@LinkedHashTreeMap.size override fun iterator(): MutableIterator { return object : LinkedTreeMapIterator() { override fun next(): K { return nextNode().key ?: throw NoSuchElementException() } } } override fun contains(element: K): Boolean { return containsKey(element) } override fun remove(element: K): Boolean { return removeInternalByKey(element) != null } override fun clear() { this@LinkedHashTreeMap.clear() } override fun add(element: K): Boolean { throw NotImplementedError() } } /** * If somebody is unlucky enough to have to serialize one of these, serialize it as a * LinkedHashMap so that they won't need Gson on the other side to deserialize it. Using * serialization defeats our DoS defence, so most apps shouldn't use it. */ private fun writeReplace(): Any = LinkedHashMap(this) } /** * Returns a new array containing the same nodes as `oldTable`, but with twice as many trees, each * of (approximately) half the previous size. */ internal fun doubleCapacity(oldTable: Array?>): Array?> { // TODO: don't do anything if we're already at MAX_CAPACITY val oldCapacity = oldTable.size // Arrays and generics don't get along. val newTable: Array?> = arrayOfNulls?>(oldCapacity * 2) val iterator = AvlIterator() val leftBuilder = AvlBuilder() val rightBuilder = AvlBuilder() // Split each tree into two trees. for (i in 0 until oldCapacity) { val root = oldTable[i] ?: continue // Compute the sizes of the left and right trees. iterator.reset(root) var leftSize = 0 var rightSize = 0 run { var node: Node? while (iterator.next().also { node = it } != null) { if (knownNotNull(node).hash and oldCapacity == 0) { leftSize++ } else { rightSize++ } } } // Split the tree into two. leftBuilder.reset(leftSize) rightBuilder.reset(rightSize) iterator.reset(root) var node: Node? while (iterator.next().also { node = it } != null) { if (knownNotNull(node).hash and oldCapacity == 0) { leftBuilder.add(knownNotNull(node)) } else { rightBuilder.add(knownNotNull(node)) } } // Populate the enlarged array with these new roots. newTable[i] = if (leftSize > 0) leftBuilder.root() else null newTable[i + oldCapacity] = if (rightSize > 0) rightBuilder.root() else null } return newTable } /** * Walks an AVL tree in iteration order. Once a node has been returned, its left, right and parent * links are **no longer used**. For this reason it is safe to transform these links as you walk a * tree. * * **Warning:** this iterator is destructive. It clears the parent node of all nodes in the tree. It * is an error to make a partial iteration of a tree. */ internal class AvlIterator { /** This stack is a singly linked list, linked by the 'parent' field. */ private var stackTop: Node? = null fun reset(root: Node?) { var stackTop: Node? = null var n = root while (n != null) { n.parent = stackTop stackTop = n // Stack push. n = n.left } this.stackTop = stackTop } operator fun next(): Node? { var stackTop: Node? = stackTop ?: return null val result = stackTop!! stackTop = result.parent result.parent = null var n = result.right while (n != null) { n.parent = stackTop stackTop = n // Stack push. n = n.left } this.stackTop = stackTop return result } } /** * Builds AVL trees of a predetermined size by accepting nodes of increasing value. To use: * 1. Call [reset] to initialize the target size *size*. * 2. Call [add] *size* times with increasing values. * 3. Call [root] to get the root of the balanced tree. * * The returned tree will satisfy the AVL constraint: for every node *N*, the height of *N.left* and * *N.right* is different by at most 1. It accomplishes this by omitting deepest-level leaf nodes * when building trees whose size isn't a power of 2 minus 1. * * Unlike rebuilding a tree from scratch, this approach requires no value comparisons. Using this * class to create a tree of size *S* is `O(S)`. */ internal class AvlBuilder { /** This stack is a singly linked list, linked by the 'parent' field. */ private var stack: Node? = null private var leavesToSkip = 0 private var leavesSkipped = 0 private var size = 0 fun reset(targetSize: Int) { // compute the target tree size. This is a power of 2 minus one, like 15 or 31. val treeCapacity = Integer.highestOneBit(targetSize) * 2 - 1 leavesToSkip = treeCapacity - targetSize size = 0 leavesSkipped = 0 stack = null } fun add(node: Node) { node.right = null node.parent = null node.left = null node.height = 1 // Skip a leaf if necessary. if (leavesToSkip > 0 && (size and 1) == 0) { size++ leavesToSkip-- leavesSkipped++ } node.parent = stack stack = node // Stack push. size++ // Skip a leaf if necessary. if (leavesToSkip > 0 && (size and 1) == 0) { size++ leavesToSkip-- leavesSkipped++ } /* * Combine 3 nodes into subtrees whenever the size is one less than a * multiple of 4. For example, we combine the nodes A, B, C into a * 3-element tree with B as the root. * * Combine two subtrees and a spare single value whenever the size is one * less than a multiple of 8. For example at 8 we may combine subtrees * (A B C) and (E F G) with D as the root to form ((A B C) D (E F G)). * * Just as we combine single nodes when size nears a multiple of 4, and * 3-element trees when size nears a multiple of 8, we combine subtrees of * size (N-1) whenever the total size is 2N-1 whenever N is a power of 2. */ var scale = 4 while (size and (scale - 1) == scale - 1) { when (leavesSkipped) { 0 -> { // Pop right, center and left, then make center the top of the stack. val right = stack!! val center = right.parent!! val left = center.parent!! center.parent = left.parent stack = center // Construct a tree. center.left = left center.right = right center.height = right.height + 1 left.parent = center right.parent = center } 1 -> { // Pop right and center, then make center the top of the stack. val right = stack!! val center = right.parent!! stack = center // Construct a tree with no left child. center.right = right center.height = right.height + 1 right.parent = center leavesSkipped = 0 } 2 -> { leavesSkipped = 0 } } scale *= 2 } } fun root(): Node { val stackTop = stack check(stackTop!!.parent == null) return stackTop } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/MapJsonAdapter.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.internal 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.rawType import java.lang.reflect.Type /** * Converts maps with string keys to JSON objects. * * TODO: support maps with other key types and convert to/from strings. */ internal class MapJsonAdapter(moshi: Moshi, keyType: Type, valueType: Type) : JsonAdapter?>() { @Suppress("UNCHECKED_CAST") private val keyAdapter: JsonAdapter = moshi.adapter(keyType).nonNull() as JsonAdapter private val valueAdapter: JsonAdapter = moshi.adapter(valueType) override fun toJson(writer: JsonWriter, value: Map?) { writer.beginObject() // Never null because we wrap in nullSafe() for ((k, v) in knownNotNull(value)) { if (k == null) { throw JsonDataException("Map key is null at ${writer.path}") } writer.promoteValueToName() keyAdapter.toJson(writer, k) valueAdapter.toJson(writer, v) } writer.endObject() } override fun fromJson(reader: JsonReader): Map { val result = LinkedHashTreeMap() reader.beginObject() while (reader.hasNext()) { reader.promoteNameToValue() val name = keyAdapter.fromJson(reader) ?: throw JsonDataException("Map key is null at ${reader.path}") val value = valueAdapter.fromJson(reader) val replaced = result.put(name, value) if (replaced != null) { throw JsonDataException( "Map key '$name' has multiple values at path ${reader.path}: $replaced and $value" ) } } reader.endObject() return result } override fun toString() = "JsonAdapter($keyAdapter=$valueAdapter)" companion object Factory : JsonAdapter.Factory { override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (annotations.isNotEmpty()) return null val rawType = type.rawType if (rawType != Map::class.java) return null val keyAndValue = mapKeyAndValueTypes(type, rawType) return MapJsonAdapter(moshi, keyAndValue[0], keyAndValue[1]).nullSafe() } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/NonNullJsonAdapter.kt ================================================ /* * Copyright (C) 2019 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 * * https://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.squareup.moshi.internal import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter public class NonNullJsonAdapter(public val delegate: JsonAdapter) : JsonAdapter() { override fun fromJson(reader: JsonReader): T & Any { return if (reader.peek() == JsonReader.Token.NULL) { throw JsonDataException("Unexpected null at " + reader.path) } else { knownNotNull(delegate.fromJson(reader)) } } override fun toJson(writer: JsonWriter, value: T?) { if (value == null) { throw JsonDataException("Unexpected null at " + writer.path) } else { delegate.toJson(writer, value) } } override fun toString(): String = "$delegate.nonNull()" } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/NullSafeJsonAdapter.kt ================================================ /* * Copyright (C) 2019 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 * * https://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.squareup.moshi.internal import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter public class NullSafeJsonAdapter(public val delegate: JsonAdapter) : JsonAdapter() { override fun fromJson(reader: JsonReader): T? { return if (reader.peek() == JsonReader.Token.NULL) { reader.nextNull() } else { delegate.fromJson(reader) } } override fun toJson(writer: JsonWriter, value: T?) { if (value == null) { writer.nullValue() } else { delegate.toJson(writer, value) } } override fun toString(): String = "$delegate.nullSafe()" } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/RecordJsonAdapter.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.internal import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import java.lang.reflect.Type /** * This is just a simple shim for linking in [StandardJsonAdapters] and swapped with a real * implementation in Java 16 via MR Jar. */ internal class RecordJsonAdapter : JsonAdapter() { override fun fromJson(reader: JsonReader) = throw AssertionError() override fun toJson(writer: JsonWriter, value: T?) = throw AssertionError() companion object Factory : JsonAdapter.Factory { override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? = null } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/StandardJsonAdapters.kt ================================================ /* * 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 * * https://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.squareup.moshi.internal 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.rawType import java.lang.reflect.Type internal object StandardJsonAdapters : JsonAdapter.Factory { private const val ERROR_FORMAT = "Expected %s but was %s at path %s" override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (annotations.isNotEmpty()) return null if (type === Boolean::class.javaPrimitiveType) return BOOLEAN_JSON_ADAPTER if (type === Byte::class.javaPrimitiveType) return BYTE_JSON_ADAPTER if (type === Char::class.javaPrimitiveType) return CHARACTER_JSON_ADAPTER if (type === Double::class.javaPrimitiveType) return DOUBLE_JSON_ADAPTER if (type === Float::class.javaPrimitiveType) return FLOAT_JSON_ADAPTER if (type === Int::class.javaPrimitiveType) return INTEGER_JSON_ADAPTER if (type === Long::class.javaPrimitiveType) return LONG_JSON_ADAPTER if (type === Short::class.javaPrimitiveType) return SHORT_JSON_ADAPTER if (type === Boolean::class.javaObjectType) return BOOLEAN_JSON_ADAPTER.nullSafe() if (type === Byte::class.javaObjectType) return BYTE_JSON_ADAPTER.nullSafe() if (type === Char::class.javaObjectType) return CHARACTER_JSON_ADAPTER.nullSafe() if (type === Double::class.javaObjectType) return DOUBLE_JSON_ADAPTER.nullSafe() if (type === Float::class.javaObjectType) return FLOAT_JSON_ADAPTER.nullSafe() if (type === Int::class.javaObjectType) return INTEGER_JSON_ADAPTER.nullSafe() if (type === Long::class.javaObjectType) return LONG_JSON_ADAPTER.nullSafe() if (type === Short::class.javaObjectType) return SHORT_JSON_ADAPTER.nullSafe() if (type === String::class.java) return STRING_JSON_ADAPTER.nullSafe() if (type === Any::class.java) return ObjectJsonAdapter(moshi).nullSafe() val rawType = type.rawType val generatedAdapter = moshi.generatedAdapter(type, rawType) if (generatedAdapter != null) { return generatedAdapter } return if (rawType.isEnum) { @Suppress("UNCHECKED_CAST") EnumJsonAdapter(rawType as Class>).nullSafe() } else { null } } fun rangeCheckNextInt(reader: JsonReader, typeMessage: String?, min: Int, max: Int): Int { val value = reader.nextInt() if (value !in min..max) { throw JsonDataException(ERROR_FORMAT.format(typeMessage, value, reader.path)) } return value } val BOOLEAN_JSON_ADAPTER: JsonAdapter = object : JsonAdapter() { override fun fromJson(reader: JsonReader) = reader.nextBoolean() override fun toJson(writer: JsonWriter, value: Boolean) { writer.value(value) } override fun toString() = "JsonAdapter(Boolean)" } private val BYTE_JSON_ADAPTER: JsonAdapter = object : JsonAdapter() { override fun fromJson(reader: JsonReader): Byte { return rangeCheckNextInt(reader, "a byte", Byte.MIN_VALUE.toInt(), 0xff).toByte() } override fun toJson(writer: JsonWriter, value: Byte) { writer.value((value.toInt() and 0xff).toLong()) } override fun toString() = "JsonAdapter(Byte)" } private val CHARACTER_JSON_ADAPTER: JsonAdapter = object : JsonAdapter() { override fun fromJson(reader: JsonReader): Char { val value = reader.nextString() if (value.length > 1) { throw JsonDataException(ERROR_FORMAT.format("a char", "\"$value\"", reader.path)) } return value[0] } override fun toJson(writer: JsonWriter, value: Char) { writer.value(value.toString()) } override fun toString() = "JsonAdapter(Character)" } private val DOUBLE_JSON_ADAPTER: JsonAdapter = object : JsonAdapter() { override fun fromJson(reader: JsonReader): Double { return reader.nextDouble() } override fun toJson(writer: JsonWriter, value: Double) { writer.value(value) } override fun toString() = "JsonAdapter(Double)" } private val FLOAT_JSON_ADAPTER: JsonAdapter = object : JsonAdapter() { override fun fromJson(reader: JsonReader): Float { val value = reader.nextDouble().toFloat() // Double check for infinity after float conversion; many doubles > Float.MAX if (!reader.lenient && value.isInfinite()) { throw JsonDataException( "JSON forbids NaN and infinities: $value at path ${reader.path}" ) } return value } override fun toJson(writer: JsonWriter, value: Float?) { // Manual null pointer check for the float.class adapter. if (value == null) { throw NullPointerException() } // Use the Number overload so we write out float precision instead of double precision. writer.value(value) } override fun toString() = "JsonAdapter(Float)" } .unsafeNonNull() private val INTEGER_JSON_ADAPTER: JsonAdapter = object : JsonAdapter() { override fun fromJson(reader: JsonReader): Int { return reader.nextInt() } override fun toJson(writer: JsonWriter, value: Int) { writer.value(value.toLong()) } override fun toString() = "JsonAdapter(Integer)" } private val LONG_JSON_ADAPTER: JsonAdapter = object : JsonAdapter() { override fun fromJson(reader: JsonReader): Long { return reader.nextLong() } override fun toJson(writer: JsonWriter, value: Long) { writer.value(value) } override fun toString() = "JsonAdapter(Long)" } private val SHORT_JSON_ADAPTER: JsonAdapter = object : JsonAdapter() { override fun fromJson(reader: JsonReader): Short { return rangeCheckNextInt( reader, "a short", Short.MIN_VALUE.toInt(), Short.MAX_VALUE.toInt(), ) .toShort() } override fun toJson(writer: JsonWriter, value: Short) { writer.value(value.toLong()) } override fun toString() = "JsonAdapter(Short)" } private val STRING_JSON_ADAPTER: JsonAdapter = object : JsonAdapter() { override fun fromJson(reader: JsonReader): String { return reader.nextString() } override fun toJson(writer: JsonWriter, value: String) { writer.value(value) } override fun toString() = "JsonAdapter(String)" } internal class EnumJsonAdapter>(private val enumType: Class) : JsonAdapter() { private val constants: Array = enumType.enumConstants private val nameStrings: Array = Array(constants.size) { i -> val constant = constants[i] val constantName = constant.name try { enumType.getField(constantName).jsonName(constantName) } catch (e: NoSuchFieldException) { throw AssertionError("Missing field in ${enumType.name}", e) } } private var options: JsonReader.Options = JsonReader.Options.of(*nameStrings) override fun fromJson(reader: JsonReader): T { val index = reader.selectString(options) if (index != -1) return constants[index] // We can consume the string safely, we are terminating anyway. val path = reader.path val name = reader.nextString() throw JsonDataException("Expected one of ${nameStrings.toList()} but was $name at path $path") } override fun toJson(writer: JsonWriter, value: T) { writer.value(nameStrings[value.ordinal]) } override fun toString() = "JsonAdapter(${enumType.name})" } /** * This adapter is used when the declared type is [Any]. Typically the runtime type is something * else, and when encoding JSON this delegates to the runtime type's adapter. For decoding (where * there is no runtime type to inspect), this uses maps and lists. * * This adapter needs a Moshi instance to look up the appropriate adapter for runtime types as * they are encountered. */ internal class ObjectJsonAdapter(private val moshi: Moshi) : JsonAdapter() { private val listJsonAdapter: JsonAdapter?> = moshi.adapter(List::class.java) private val mapAdapter: JsonAdapter?> = moshi.adapter(Map::class.java) private val stringAdapter: JsonAdapter = moshi.adapter(String::class.java) private val doubleAdapter: JsonAdapter = moshi.adapter(Double::class.java) private val booleanAdapter: JsonAdapter = moshi.adapter(Boolean::class.java) override fun fromJson(reader: JsonReader): Any? { return when (reader.peek()) { JsonReader.Token.BEGIN_ARRAY -> listJsonAdapter.fromJson(reader) JsonReader.Token.BEGIN_OBJECT -> mapAdapter.fromJson(reader) JsonReader.Token.STRING -> stringAdapter.fromJson(reader) JsonReader.Token.NUMBER -> doubleAdapter.fromJson(reader) JsonReader.Token.BOOLEAN -> booleanAdapter.fromJson(reader) JsonReader.Token.NULL -> reader.nextNull() else -> throw IllegalStateException( "Expected a value but was ${reader.peek()} at path ${reader.path}" ) } } override fun toJson(writer: JsonWriter, value: Any?) { when (val valueClass: Class<*>? = value?.javaClass) { Any::class.java -> { // Don't recurse infinitely when the runtime type is also Object.class. writer.beginObject() writer.endObject() } null -> { writer.nullValue() } else -> { moshi.adapter(toJsonType(valueClass), NO_ANNOTATIONS).toJson(writer, value) } } } /** * Returns the type to look up a type adapter for when writing `value` to JSON. Without this, * attempts to emit standard types like `LinkedHashMap` would fail because Moshi doesn't provide * built-in adapters for implementation types. It knows how to **write** those types, but lacks * a mechanism to read them because it doesn't know how to find the appropriate constructor. */ private fun toJsonType(valueClass: Class<*>): Class<*> { if (Map::class.java.isAssignableFrom(valueClass)) return Map::class.java return if (Collection::class.java.isAssignableFrom(valueClass)) { Collection::class.java } else { valueClass } } override fun toString() = "JsonAdapter(Object)" } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/internal/Util.kt ================================================ /* * 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 * * https://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:JvmName("Util") @file:Suppress("unused", "MemberVisibilityCanBePrivate") package com.squareup.moshi.internal import com.squareup.moshi.Json import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonQualifier import com.squareup.moshi.JsonReader import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.moshi.Types.getRawType import com.squareup.moshi.asArrayType import com.squareup.moshi.rawType import java.lang.reflect.AnnotatedElement import java.lang.reflect.Constructor import java.lang.reflect.GenericArrayType import java.lang.reflect.InvocationTargetException import java.lang.reflect.ParameterizedType import java.lang.reflect.Proxy import java.lang.reflect.Type import java.lang.reflect.TypeVariable import java.lang.reflect.WildcardType import java.util.Collections import java.util.Properties import kotlin.contracts.contract @JvmField internal val NO_ANNOTATIONS: Set = emptySet() @JvmField internal val EMPTY_TYPE_ARRAY: Array = arrayOf() @Suppress("UNCHECKED_CAST") private val METADATA: Class? = try { Class.forName(kotlinMetadataClassName) as Class } catch (_: ClassNotFoundException) { null } // We look up the constructor marker separately because Metadata might be (justifiably) // stripped by R8/Proguard but the DefaultConstructorMarker is still present. @JvmField public val DEFAULT_CONSTRUCTOR_MARKER: Class<*>? = try { Class.forName("kotlin.jvm.internal.DefaultConstructorMarker") } catch (_: ClassNotFoundException) { null } /** A map from primitive types to their corresponding wrapper types. */ private val PRIMITIVE_TO_WRAPPER_TYPE: Map, Class<*>> = buildMap(16) { put(knownNotNull(Boolean::class.javaPrimitiveType), Boolean::class.java) put(knownNotNull(Byte::class.javaPrimitiveType), Byte::class.java) put(knownNotNull(Char::class.javaPrimitiveType), Char::class.java) put(knownNotNull(Double::class.javaPrimitiveType), Double::class.java) put(knownNotNull(Float::class.javaPrimitiveType), Float::class.java) put(knownNotNull(Int::class.javaPrimitiveType), Int::class.java) put(knownNotNull(Long::class.javaPrimitiveType), Long::class.java) put(knownNotNull(Short::class.javaPrimitiveType), Short::class.java) put(Void.TYPE, Void::class.java) } // Extracted as a method with a keep rule to prevent R8 from keeping Kotlin Metadata private val kotlinMetadataClassName: String get() = "kotlin.Metadata" public fun AnnotatedElement.jsonName(declaredName: String): String { return getAnnotation(Json::class.java).jsonName(declaredName) } internal fun Json?.jsonName(declaredName: String): String { if (this == null) return declaredName val annotationName: String = name return if (Json.UNSET_NAME == annotationName) declaredName else annotationName } internal val AnnotatedElement.jsonAnnotations: Set get() = annotations.jsonAnnotations public val Array.jsonAnnotations: Set get() { var result: MutableSet? = null for (annotation in this) { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") if ( (annotation as java.lang.annotation.Annotation) .annotationType() .isAnnotationPresent(JsonQualifier::class.java) ) { if (result == null) result = LinkedHashSet() result.add(annotation) } } return if (result != null) Collections.unmodifiableSet(result) else NO_ANNOTATIONS } internal fun Set.isAnnotationPresent(annotationClass: Class): Boolean { if (isEmpty()) return false // Save an iterator in the common case. for (annotation in this) { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") if ((annotation as java.lang.annotation.Annotation).annotationType() == annotationClass) { return true } } return false } /** Returns true if `annotations` has any annotation whose simple name is Nullable. */ internal val Array.hasNullable: Boolean get() { for (annotation in this) { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") if ( (annotation as java.lang.annotation.Annotation).annotationType().simpleName == "Nullable" ) { return true } } return false } /** * Returns true if `rawType` is built in. We don't reflect on private fields of platform types * because they're unspecified and likely to be different on Java vs. Android. */ public val Class<*>.isPlatformType: Boolean get() { val name = name return (name.startsWith("android.") || name.startsWith("androidx.") || name.startsWith("java.") || name.startsWith("javax.") || // L8 desugared APIs name.startsWith("j$.") || name.startsWith("kotlin.") || name.startsWith("kotlinx.") || name.startsWith("scala.")) } /** Throws the cause of `e`, wrapping it if it is checked. */ internal fun InvocationTargetException.rethrowCause(): RuntimeException { val cause = targetException if (cause is RuntimeException) throw cause if (cause is Error) throw cause throw RuntimeException(cause) } /** * Returns a type that is functionally equal but not necessarily equal according to * [[Object.equals()]][Object.equals]. */ internal fun Type.canonicalize(): Type { return when (this) { is Class<*> -> { if (isArray) GenericArrayTypeImpl(componentType.canonicalize()) else this } is ParameterizedType -> { if (this is ParameterizedTypeImpl) return this ParameterizedTypeImpl(ownerType, rawType, actualTypeArguments) } is GenericArrayType -> { if (this is GenericArrayTypeImpl) return this GenericArrayTypeImpl(genericComponentType) } is WildcardType -> { if (this is WildcardTypeImpl) return this WildcardTypeImpl(upperBounds, lowerBounds) } else -> this // This type is unsupported! } } /** If type is a "? extends X" wildcard, returns X; otherwise returns type unchanged. */ internal fun Type.removeSubtypeWildcard(): Type { if (this !is WildcardType) return this val lowerBounds = lowerBounds if (lowerBounds.isNotEmpty()) return this val upperBounds = upperBounds require(upperBounds.size == 1) return upperBounds[0] } public fun Type.resolve(context: Type, contextRawType: Class<*>): Type { return this.resolve(context, contextRawType, LinkedHashSet()) } private fun Type.resolve( context: Type, contextRawType: Class<*>, visitedTypeVariables: MutableCollection>, ): Type { // This implementation is made a little more complicated in an attempt to avoid object-creation. var toResolve = this while (true) { when { toResolve is TypeVariable<*> -> { val typeVariable = toResolve if (typeVariable in visitedTypeVariables) { // cannot reduce due to infinite recursion return toResolve } else { visitedTypeVariables += typeVariable } toResolve = resolveTypeVariable(context, contextRawType, typeVariable) if (toResolve === typeVariable) return toResolve } toResolve is Class<*> && toResolve.isArray -> { val original = toResolve val componentType: Type = original.componentType val newComponentType = componentType.resolve(context, contextRawType, visitedTypeVariables) return if (componentType === newComponentType) original else newComponentType.asArrayType() } toResolve is GenericArrayType -> { val original = toResolve val componentType = original.genericComponentType val newComponentType = componentType.resolve(context, contextRawType, visitedTypeVariables) return if (componentType === newComponentType) original else newComponentType.asArrayType() } toResolve is ParameterizedType -> { val original = toResolve val ownerType: Type? = original.ownerType val newOwnerType = ownerType?.let { ownerType.resolve(context, contextRawType, visitedTypeVariables) } var changed = newOwnerType !== ownerType var args = original.actualTypeArguments for (t in args.indices) { val resolvedTypeArgument = args[t].resolve(context, contextRawType, visitedTypeVariables) if (resolvedTypeArgument !== args[t]) { if (!changed) { args = args.clone() changed = true } args[t] = resolvedTypeArgument } } return if (changed) { ParameterizedTypeImpl(newOwnerType, original.rawType, args) } else { original } } toResolve is WildcardType -> { val original = toResolve val originalLowerBound = original.lowerBounds val originalUpperBound = original.upperBounds if (originalLowerBound.size == 1) { val lowerBound = originalLowerBound[0].resolve(context, contextRawType, visitedTypeVariables) if (lowerBound !== originalLowerBound[0]) { return Types.supertypeOf(lowerBound) } } else if (originalUpperBound.size == 1) { val upperBound = originalUpperBound[0].resolve(context, contextRawType, visitedTypeVariables) if (upperBound !== originalUpperBound[0]) { return Types.subtypeOf(upperBound) } } return original } else -> return toResolve } } } internal fun resolveTypeVariable( context: Type, contextRawType: Class<*>, unknown: TypeVariable<*>, ): Type { val declaredByRaw = declaringClassOf(unknown) ?: return unknown // We can't reduce this further. val declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw) if (declaredBy is ParameterizedType) { val index = declaredByRaw.typeParameters.indexOf(unknown) return declaredBy.actualTypeArguments[index] } return unknown } /** * Returns the generic supertype for `supertype`. For example, given a class `IntegerSet`, the * result for when supertype is `Set.class` is `Set` and the result when the supertype is * `Collection.class` is `Collection`. */ internal fun getGenericSupertype( context: Type, rawTypeInitial: Class<*>, toResolve: Class<*>, ): Type { var rawType = rawTypeInitial if (toResolve == rawType) { return context } // we skip searching through interfaces if unknown is an interface if (toResolve.isInterface) { val interfaces = rawType.interfaces for (i in interfaces.indices) { if (interfaces[i] == toResolve) { return rawType.genericInterfaces[i] } else if (toResolve.isAssignableFrom(interfaces[i])) { return getGenericSupertype(rawType.genericInterfaces[i], interfaces[i], toResolve) } } } // check our supertypes if (!rawType.isInterface) { while (rawType != Any::class.java) { val rawSupertype = rawType.superclass if (rawSupertype == toResolve) { return rawType.genericSuperclass } else if (toResolve.isAssignableFrom(rawSupertype)) { return getGenericSupertype(rawType.genericSuperclass, rawSupertype, toResolve) } rawType = rawSupertype } } // we can't resolve this further return toResolve } internal val Any?.hashCodeOrZero: Int get() { return this?.hashCode() ?: 0 } internal fun Type.typeToString(): String { return if (this is Class<*>) name else toString() } /** Returns the declaring class of `typeVariable`, or `null` if it was not declared by a class. */ internal fun declaringClassOf(typeVariable: TypeVariable<*>): Class<*>? { val genericDeclaration = typeVariable.genericDeclaration return genericDeclaration as? Class<*> } internal fun Type.checkNotPrimitive() { require(this !is Class<*> || !isPrimitive) { "Unexpected primitive $this. Use the boxed type." } } internal fun Type.toStringWithAnnotations(annotations: Set): String { return when { annotations.isEmpty() -> "$this (with no annotations)" else -> "$this annotated $annotations" } } /** * Returns the element type of `type` if it is an array type, or null if it is not an array type. */ internal fun arrayComponentType(type: Type): Type? { return when (type) { is GenericArrayType -> type.genericComponentType is Class<*> -> type.componentType else -> null } } internal fun getGenericSuperclass(type: Type): Type { val rawType = getRawType(type) return rawType.genericSuperclass.resolve(type, rawType) } /** * Returns a two element array containing this map's key and value types in positions 0 and 1 * respectively. */ internal fun mapKeyAndValueTypes(context: Type, contextRawType: Class<*>): Array { // Work around a problem with the declaration of java.util.Properties. That class should extend // Hashtable, but it's declared to extend Hashtable. if (context === Properties::class.java) return arrayOf(String::class.java, String::class.java) val mapType = getSupertype(context, contextRawType, MutableMap::class.java) if (mapType is ParameterizedType) { return mapType.actualTypeArguments } return arrayOf(Any::class.java, Any::class.java) } /** * Returns the generic form of `supertype`. For example, if this is `ArrayList`, this * returns `Iterable` given the input `Iterable.class`. * * @param supertype a superclass of, or interface implemented by, this. */ internal fun getSupertype(context: Type, contextRawType: Class<*>, supertype: Class<*>): Type { require(supertype.isAssignableFrom(contextRawType)) return getGenericSupertype(context, contextRawType, supertype).resolve(context, contextRawType) } internal fun createJsonQualifierImplementation(annotationType: Class): T { require(annotationType.isAnnotation) { "$annotationType must be an annotation." } require(annotationType.isAnnotationPresent(JsonQualifier::class.java)) { "$annotationType must have @JsonQualifier." } require(annotationType.declaredMethods.isEmpty()) { "$annotationType must not declare methods." } @Suppress("UNCHECKED_CAST") return Proxy.newProxyInstance(annotationType.classLoader, arrayOf>(annotationType)) { proxy, method, args -> when (method.name) { "annotationType" -> annotationType "equals" -> { val o = args[0] annotationType.isInstance(o) } "hashCode" -> 0 "toString" -> "@${annotationType.name}()" else -> method.invoke(proxy, *args) } } as T } /** * Loads the generated JsonAdapter for classes annotated [JsonClass]. This works because it uses the * same naming conventions as `JsonClassCodeGenProcessor`. */ public fun Moshi.generatedAdapter(type: Type, rawType: Class<*>): JsonAdapter<*>? { val jsonClass = rawType.getAnnotation(JsonClass::class.java) if (jsonClass == null || !jsonClass.generateAdapter) { return null } val adapterClassName = Types.generatedJsonAdapterName(rawType.name) var possiblyFoundAdapter: Class>? = null return try { @Suppress("UNCHECKED_CAST") val adapterClass = Class.forName(adapterClassName, true, rawType.classLoader) as Class> possiblyFoundAdapter = adapterClass var constructor: Constructor> var args: Array if (type is ParameterizedType) { val typeArgs = type.actualTypeArguments try { // Common case first constructor = adapterClass.getDeclaredConstructor(Moshi::class.java, Array::class.java) args = arrayOf(this, typeArgs) } catch (_: NoSuchMethodException) { constructor = adapterClass.getDeclaredConstructor(Array::class.java) args = arrayOf(typeArgs) } } else { try { // Common case first constructor = adapterClass.getDeclaredConstructor(Moshi::class.java) args = arrayOf(this) } catch (_: NoSuchMethodException) { constructor = adapterClass.getDeclaredConstructor() args = emptyArray() } } constructor.isAccessible = true constructor.newInstance(*args).nullSafe() } catch (e: ClassNotFoundException) { throw RuntimeException("Failed to find the generated JsonAdapter class for $type", e) } catch (e: NoSuchMethodException) { if ( possiblyFoundAdapter != null && type !is ParameterizedType && possiblyFoundAdapter.typeParameters.isNotEmpty() ) { throw RuntimeException( "Failed to find the generated JsonAdapter constructor for '$type'. Suspiciously, the type was not parameterized but the target class '${possiblyFoundAdapter.canonicalName}' is generic. Consider using Types#newParameterizedType() to define these missing type variables.", e, ) } else { throw RuntimeException("Failed to find the generated JsonAdapter constructor for $type", e) } } catch (e: IllegalAccessException) { throw RuntimeException("Failed to access the generated JsonAdapter for $type", e) } catch (e: InstantiationException) { throw RuntimeException("Failed to instantiate the generated JsonAdapter for $type", e) } catch (e: InvocationTargetException) { throw e.rethrowCause() } } internal val Class<*>.isKotlin: Boolean get() = METADATA != null && isAnnotationPresent(METADATA) /** * Reflectively looks up the defaults constructor of a kotlin class. * * @param T the type of `targetClass`. * @return the instantiated `targetClass` instance. * @receiver the target kotlin class to instantiate. */ internal fun Class.lookupDefaultsConstructor(): Constructor { checkNotNull(DEFAULT_CONSTRUCTOR_MARKER) { "DefaultConstructorMarker not on classpath. Make sure the Kotlin stdlib is on the classpath." } val defaultConstructor = findConstructor() defaultConstructor.isAccessible = true return defaultConstructor } private fun Class.findConstructor(): Constructor { for (constructor in declaredConstructors) { val paramTypes = constructor.parameterTypes if (paramTypes.isNotEmpty() && paramTypes[paramTypes.size - 1] == DEFAULT_CONSTRUCTOR_MARKER) { @Suppress("UNCHECKED_CAST") return constructor as Constructor } } throw IllegalStateException("No defaults constructor found for $this") } public fun missingProperty( propertyName: String?, jsonName: String?, reader: JsonReader, ): JsonDataException { val path = reader.path val message = if (jsonName == propertyName) { "Required value '$propertyName' missing at $path" } else { "Required value '$propertyName' (JSON name '$jsonName') missing at $path" } return JsonDataException(message) } public fun unexpectedNull( propertyName: String, jsonName: String, reader: JsonReader, ): JsonDataException { val path = reader.path val message: String = if (jsonName == propertyName) { "Non-null value '$propertyName' was null at $path" } else { "Non-null value '$propertyName' (JSON name '$jsonName') was null at $path" } return JsonDataException(message) } // Sneaky backdoor way of marking a value as non-null to the compiler and skip the null-check // intrinsic. // Safe to use (unstable) contracts since they're gone in the final bytecode @Suppress("NOTHING_TO_INLINE") internal inline fun markNotNull(value: T?) { contract { returns() implies (value != null) } } @Suppress("NOTHING_TO_INLINE") internal inline fun knownNotNull(value: T?): T { markNotNull(value) return value } // Public due to inline access in MoshiKotlinTypesExtensions @PublishedApi internal fun Class.boxIfPrimitive(): Class { // cast is safe: long.class and Long.class are both of type Class @Suppress("UNCHECKED_CAST") val wrapped = PRIMITIVE_TO_WRAPPER_TYPE[this] as Class? return wrapped ?: this } internal inline fun checkNull(value: T?, lazyMessage: (T) -> Any) { if (value != null) { val message = lazyMessage(value) throw IllegalStateException(message.toString()) } } internal class ParameterizedTypeImpl( ownerType: Type?, rawType: Type, typeArguments: Array, ) : ParameterizedType { private val ownerType: Type? = ownerType?.canonicalize() private val rawType: Type = rawType.canonicalize() @JvmField val typeArguments: Array = Array(typeArguments.size) { index -> typeArguments[index].canonicalize().also { it.checkNotPrimitive() } } init { // Require an owner type if the raw type needs it. if (rawType is Class<*>) { if (ownerType != null) { require(ownerType.rawType == rawType.enclosingClass) { "unexpected owner type for $rawType: $ownerType" } } else { require(rawType.enclosingClass == null) { "unexpected owner type for $rawType: null" } } } } override fun getActualTypeArguments() = typeArguments.clone() override fun getRawType() = rawType override fun getOwnerType() = ownerType @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") override fun equals(other: Any?) = other is ParameterizedType && Types.equals(this, other as ParameterizedType?) override fun hashCode(): Int { return typeArguments.contentHashCode() xor rawType.hashCode() xor ownerType.hashCodeOrZero } override fun toString(): String { // TODO(jwilson): include the owner type if it's non-null. val result = StringBuilder(30 * (typeArguments.size + 1)) result.append(rawType.typeToString()) if (typeArguments.isEmpty()) { return result.toString() } result.append("<").append(typeArguments[0].typeToString()) for (i in 1 until typeArguments.size) { result.append(", ").append(typeArguments[i].typeToString()) } return result.append(">").toString() } } internal class GenericArrayTypeImpl(componentType: Type) : GenericArrayType { private val componentType: Type = componentType.canonicalize() override fun getGenericComponentType() = componentType @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") override fun equals(other: Any?) = other is GenericArrayType && Types.equals(this, other as GenericArrayType?) override fun hashCode() = componentType.hashCode() override fun toString() = "${componentType.typeToString()}[]" } /** * The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only * support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper * bound must be Object.class. */ internal class WildcardTypeImpl(upperBound: Type, lowerBound: Type?) : WildcardType { private val upperBound: Type = upperBound.canonicalize() private val lowerBound: Type? = lowerBound?.canonicalize() constructor( upperBounds: Array, lowerBounds: Array, ) : this(upperBounds.single(), lowerBounds.getOrNull(0)) { require(lowerBounds.size <= 1) } init { require(lowerBound == null || upperBound === Any::class.java) upperBound.checkNotPrimitive() lowerBound?.checkNotPrimitive() } override fun getUpperBounds() = arrayOf(upperBound) override fun getLowerBounds() = lowerBound?.let { arrayOf(it) } ?: EMPTY_TYPE_ARRAY @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") override fun equals(other: Any?) = other is WildcardType && Types.equals(this, other as WildcardType?) override fun hashCode(): Int { // This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()). return (if (lowerBound != null) 31 + lowerBound.hashCode() else 1) xor 31 + upperBound.hashCode() } override fun toString(): String { return when { lowerBound != null -> "? super ${lowerBound.typeToString()}" upperBound === Any::class.java -> "?" else -> "? extends ${upperBound.typeToString()}" } } } ================================================ FILE: moshi/src/main/java/com/squareup/moshi/package-info.java ================================================ /** Moshi is modern JSON library for Android and Java. */ @javax.annotation.ParametersAreNonnullByDefault package com.squareup.moshi; ================================================ FILE: moshi/src/main/java16/com/squareup/moshi/internal/RecordJsonAdapter.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.internal import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.rawType import java.lang.invoke.MethodHandle import java.lang.invoke.MethodHandles import java.lang.invoke.MethodType.methodType import java.lang.reflect.InvocationTargetException import java.lang.reflect.ParameterizedType import java.lang.reflect.RecordComponent import java.lang.reflect.Type /** * A [JsonAdapter] that supports Java `record` classes via reflection. * * **NOTE:** Java records require JDK 16 or higher. */ internal class RecordJsonAdapter( private val constructor: MethodHandle, private val targetClass: String, componentBindings: Map>, ) : JsonAdapter() { data class ComponentBinding( val componentName: String, val jsonName: String, val adapter: JsonAdapter, val accessor: MethodHandle, ) private val componentBindingsArray = componentBindings.values.toTypedArray() private val options = JsonReader.Options.of(*componentBindings.keys.toTypedArray()) override fun fromJson(reader: JsonReader): T? { val resultsArray = arrayOfNulls(componentBindingsArray.size) reader.beginObject() while (reader.hasNext()) { val index = reader.selectName(options) if (index == -1) { reader.skipName() reader.skipValue() continue } resultsArray[index] = componentBindingsArray[index].adapter.fromJson(reader) } reader.endObject() return try { @Suppress("UNCHECKED_CAST") constructor.invokeWithArguments(*resultsArray) as T } catch (e: InvocationTargetException) { throw e.rethrowCause() } catch (e: Throwable) { // Don't throw a fatal error if it's just an absent primitive. for (i in componentBindingsArray.indices) { if ( resultsArray[i] == null && componentBindingsArray[i].accessor.type().returnType().isPrimitive ) { throw missingProperty( propertyName = componentBindingsArray[i].componentName, jsonName = componentBindingsArray[i].jsonName, reader = reader, ) } } throw AssertionError(e) } } override fun toJson(writer: JsonWriter, value: T?) { writer.beginObject() for (binding in componentBindingsArray) { writer.name(binding.jsonName) val componentValue = try { binding.accessor.invoke(value) } catch (e: InvocationTargetException) { throw e.rethrowCause() } catch (e: Throwable) { throw AssertionError(e) } binding.adapter.toJson(writer, componentValue) } writer.endObject() } override fun toString() = "JsonAdapter($targetClass)" companion object Factory : JsonAdapter.Factory { private val VOID_CLASS = knownNotNull(Void::class.javaPrimitiveType) override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (annotations.isNotEmpty()) { return null } if (type !is Class<*> && type !is ParameterizedType) { return null } val rawType = type.rawType if (!rawType.isRecord) { return null } val components = rawType.recordComponents val bindings = LinkedHashMap>() val lookup = MethodHandles.lookup() val componentRawTypes = Array>(components.size) { i -> val component = components[i] val componentBinding = createComponentBinding(type, rawType, moshi, lookup, component) val replaced = bindings.put(componentBinding.jsonName, componentBinding) if (replaced != null) { throw IllegalArgumentException( "Conflicting components:\n ${replaced.componentName}\n ${componentBinding.componentName}" ) } component.type } val constructor = try { lookup.findConstructor(rawType, methodType(VOID_CLASS, componentRawTypes)) } catch (e: NoSuchMethodException) { throw AssertionError(e) } catch (e: IllegalAccessException) { throw AssertionError(e) } return RecordJsonAdapter(constructor, rawType.simpleName, bindings).nullSafe() } private fun createComponentBinding( type: Type, rawType: Class<*>, moshi: Moshi, lookup: MethodHandles.Lookup, component: RecordComponent, ): ComponentBinding { val componentName = component.name val jsonName = component.jsonName(componentName) val componentType = component.genericType.resolve(type, rawType) val qualifiers = component.jsonAnnotations val adapter = moshi.adapter(componentType, qualifiers) val accessor = try { lookup.unreflect(component.accessor) } catch (e: IllegalAccessException) { throw AssertionError(e) } return ComponentBinding(componentName, jsonName, adapter, accessor) } } } ================================================ FILE: moshi/src/main/resources/META-INF/proguard/moshi.pro ================================================ # JSR 305 annotations are for embedding nullability information. -dontwarn javax.annotation.** -keepclasseswithmembers class * { @com.squareup.moshi.* ; } -keep @com.squareup.moshi.JsonQualifier @interface * # Enum field names are used by the integrated EnumJsonAdapter. # values() is synthesized by the Kotlin compiler and is used by EnumJsonAdapter indirectly # Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi. -keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum { ; **[] values(); } # Keep helper method to avoid R8 optimisation that would keep all Kotlin Metadata when unwanted -keepclassmembers class com.squareup.moshi.internal.Util { private static java.lang.String getKotlinMetadataClassName(); } # Keep ToJson/FromJson-annotated methods -keepclassmembers class * { @com.squareup.moshi.FromJson ; @com.squareup.moshi.ToJson ; } ================================================ FILE: moshi/src/test/java/android/util/Pair.java ================================================ /* * Copyright (C) 2020 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 * * https://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 android.util; public final class Pair {} ================================================ FILE: moshi/src/test/java/com/squareup/moshi/AdapterMethodsTest.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.Assert.fail; import com.squareup.moshi.MoshiTest.Uppercase; import com.squareup.moshi.MoshiTest.UppercaseAdapterFactory; import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import okio.ByteString; import org.junit.Test; public final class AdapterMethodsTest { @Test public void toAndFromJsonViaListOfIntegers() throws Exception { Moshi moshi = new Moshi.Builder().add(new PointAsListOfIntegersJsonAdapter()).build(); JsonAdapter pointAdapter = moshi.adapter(Point.class); assertThat(pointAdapter.toJson(new Point(5, 8))).isEqualTo("[5,8]"); assertThat(pointAdapter.fromJson("[5,8]")).isEqualTo(new Point(5, 8)); } static class PointAsListOfIntegersJsonAdapter { @ToJson List pointToJson(Point point) { return Arrays.asList(point.x, point.y); } @FromJson Point pointFromJson(List o) throws Exception { if (o.size() != 2) throw new Exception("Expected 2 elements but was " + o); return new Point(o.get(0), o.get(1)); } } @Test public void toAndFromJsonWithWriterAndReader() throws Exception { Moshi moshi = new Moshi.Builder().add(new PointWriterAndReaderJsonAdapter()).build(); JsonAdapter pointAdapter = moshi.adapter(Point.class); assertThat(pointAdapter.toJson(new Point(5, 8))).isEqualTo("[5,8]"); assertThat(pointAdapter.fromJson("[5,8]")).isEqualTo(new Point(5, 8)); } static class PointWriterAndReaderJsonAdapter { @ToJson void pointToJson(JsonWriter writer, Point point) throws IOException { writer.beginArray(); writer.value(point.x); writer.value(point.y); writer.endArray(); } @FromJson Point pointFromJson(JsonReader reader) throws Exception { reader.beginArray(); int x = reader.nextInt(); int y = reader.nextInt(); reader.endArray(); return new Point(x, y); } } private static final class PointJsonAdapterWithDelegate { @FromJson Point fromJson(JsonReader reader, JsonAdapter delegate) throws IOException { reader.beginArray(); Point value = delegate.fromJson(reader); reader.endArray(); return value; } @ToJson void toJson(JsonWriter writer, Point value, JsonAdapter delegate) throws IOException { writer.beginArray(); delegate.toJson(writer, value); writer.endArray(); } } private static final class PointJsonAdapterWithDelegateWithQualifier { @FromJson @WithParens Point fromJson(JsonReader reader, @WithParens JsonAdapter delegate) throws IOException { reader.beginArray(); Point value = delegate.fromJson(reader); reader.endArray(); return value; } @ToJson void toJson(JsonWriter writer, @WithParens Point value, @WithParens JsonAdapter delegate) throws IOException { writer.beginArray(); delegate.toJson(writer, value); writer.endArray(); } } @Test public void toAndFromWithDelegate() throws Exception { Moshi moshi = new Moshi.Builder().add(new PointJsonAdapterWithDelegate()).build(); JsonAdapter adapter = moshi.adapter(Point.class); Point point = new Point(5, 8); assertThat(adapter.toJson(point)).isEqualTo("[{\"x\":5,\"y\":8}]"); assertThat(adapter.fromJson("[{\"x\":5,\"y\":8}]")).isEqualTo(point); } @Test public void toAndFromWithDelegateWithQualifier() throws Exception { Moshi moshi = new Moshi.Builder() .add(new PointJsonAdapterWithDelegateWithQualifier()) .add(new PointWithParensJsonAdapter()) .build(); JsonAdapter adapter = moshi.adapter(Point.class, WithParens.class); Point point = new Point(5, 8); assertThat(adapter.toJson(point)).isEqualTo("[\"(5 8)\"]"); assertThat(adapter.fromJson("[\"(5 8)\"]")).isEqualTo(point); } @Test public void toAndFromWithIntermediate() throws Exception { Moshi moshi = new Moshi.Builder() .add( new Object() { @FromJson String fromJson(String string) { return string.substring(1, string.length() - 1); } @ToJson String toJson(String value) { return "|" + value + "|"; } }) .build(); JsonAdapter adapter = moshi.adapter(String.class); assertThat(adapter.toJson("pizza")).isEqualTo("\"|pizza|\""); assertThat(adapter.fromJson("\"|pizza|\"")).isEqualTo("pizza"); } @Test public void toAndFromWithIntermediateWithQualifier() throws Exception { Moshi moshi = new Moshi.Builder() .add( new Object() { @FromJson @Uppercase String fromJson(@Uppercase String string) { return string.substring(1, string.length() - 1); } @ToJson @Uppercase String toJson(@Uppercase String value) { return "|" + value + "|"; } }) .add(new UppercaseAdapterFactory()) .build(); JsonAdapter adapter = moshi.adapter(String.class, Uppercase.class); assertThat(adapter.toJson("pizza")).isEqualTo("\"|PIZZA|\""); assertThat(adapter.fromJson("\"|pizza|\"")).isEqualTo("PIZZA"); } @Test public void toJsonOnly() throws Exception { Moshi moshi = new Moshi.Builder().add(new PointAsListOfIntegersToAdapter()).build(); JsonAdapter pointAdapter = moshi.adapter(Point.class); assertThat(pointAdapter.toJson(new Point(5, 8))).isEqualTo("[5,8]"); assertThat(pointAdapter.fromJson("{\"x\":5,\"y\":8}")).isEqualTo(new Point(5, 8)); } static class PointAsListOfIntegersToAdapter { @ToJson List pointToJson(Point point) { return Arrays.asList(point.x, point.y); } } @Test public void fromJsonOnly() throws Exception { Moshi moshi = new Moshi.Builder().add(new PointAsListOfIntegersFromAdapter()).build(); JsonAdapter pointAdapter = moshi.adapter(Point.class); assertThat(pointAdapter.toJson(new Point(5, 8))).isEqualTo("{\"x\":5,\"y\":8}"); assertThat(pointAdapter.fromJson("[5,8]")).isEqualTo(new Point(5, 8)); } static class PointAsListOfIntegersFromAdapter { @FromJson Point pointFromJson(List o) throws Exception { if (o.size() != 2) throw new Exception("Expected 2 elements but was " + o); return new Point(o.get(0), o.get(1)); } } @Test public void multipleLayersOfAdapters() throws Exception { Moshi moshi = new Moshi.Builder().add(new MultipleLayersJsonAdapter()).build(); JsonAdapter pointAdapter = moshi.adapter(Point.class).lenient(); assertThat(pointAdapter.toJson(new Point(5, 8))).isEqualTo("\"5 8\""); assertThat(pointAdapter.fromJson("\"5 8\"")).isEqualTo(new Point(5, 8)); } static class MultipleLayersJsonAdapter { @ToJson List pointToJson(Point point) { return Arrays.asList(point.x, point.y); } @ToJson String integerListToJson(List list) { StringBuilder result = new StringBuilder(); for (Integer i : list) { if (result.length() != 0) result.append(" "); result.append(i.intValue()); } return result.toString(); } @FromJson Point pointFromJson(List o) throws Exception { if (o.size() != 2) throw new Exception("Expected 2 elements but was " + o); return new Point(o.get(0), o.get(1)); } @FromJson List listOfIntegersFromJson(String list) throws Exception { List result = new ArrayList<>(); for (String part : list.split(" ")) { result.add(Integer.parseInt(part)); } return result; } } @Test public void conflictingToAdapters() throws Exception { Moshi.Builder builder = new Moshi.Builder(); try { builder.add(new ConflictingsToJsonAdapter()); fail(); } catch (IllegalStateException expected) { assertThat(expected.getMessage()).contains("Conflicting @ToJson methods:"); assertThat(expected.getMessage()).contains("pointToJson1"); assertThat(expected.getMessage()).contains("pointToJson2"); } } static class ConflictingsToJsonAdapter { @ToJson List pointToJson1(Point point) { throw new AssertionError(); } @ToJson String pointToJson2(Point point) { throw new AssertionError(); } } @Test public void conflictingFromAdapters() throws Exception { Moshi.Builder builder = new Moshi.Builder(); try { builder.add(new ConflictingsFromJsonAdapter()); fail(); } catch (IllegalStateException expected) { assertThat(expected.getMessage()).contains("Conflicting @FromJson methods:"); assertThat(expected.getMessage()).contains("pointFromJson1"); assertThat(expected.getMessage()).contains("pointFromJson2"); } } static class ConflictingsFromJsonAdapter { @FromJson Point pointFromJson1(List point) { throw new AssertionError(); } @FromJson Point pointFromJson2(String point) { throw new AssertionError(); } } @Test public void emptyAdapters() throws Exception { Moshi.Builder builder = new Moshi.Builder(); try { builder.add(new EmptyJsonAdapter()).build(); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Expected at least one @ToJson or @FromJson method on " + "com.squareup.moshi.AdapterMethodsTest$EmptyJsonAdapter"); } } static class EmptyJsonAdapter {} @Test public void unexpectedSignatureToAdapters() throws Exception { Moshi.Builder builder = new Moshi.Builder(); try { builder.add(new UnexpectedSignatureToJsonAdapter()).build(); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Unexpected signature for void " + "com.squareup.moshi.AdapterMethodsTest$UnexpectedSignatureToJsonAdapter.pointToJson" + "(com.squareup.moshi.AdapterMethodsTest$Point).\n" + "@ToJson method signatures may have one of the following structures:\n" + " void toJson(JsonWriter writer, T value) throws ;\n" + " void toJson(JsonWriter writer, T value," + " JsonAdapter delegate, ) throws ;\n" + " R toJson(T value) throws ;\n"); } } static class UnexpectedSignatureToJsonAdapter { @ToJson void pointToJson(Point point) {} } @Test public void unexpectedSignatureFromAdapters() throws Exception { Moshi.Builder builder = new Moshi.Builder(); try { builder.add(new UnexpectedSignatureFromJsonAdapter()).build(); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Unexpected signature for void " + "com.squareup.moshi.AdapterMethodsTest$UnexpectedSignatureFromJsonAdapter.pointFromJson" + "(java.lang.String).\n" + "@FromJson method signatures may have one of the following structures:\n" + " R fromJson(JsonReader jsonReader) throws ;\n" + " R fromJson(JsonReader jsonReader," + " JsonAdapter delegate, ) throws ;\n" + " R fromJson(T value) throws ;\n"); } } static class UnexpectedSignatureFromJsonAdapter { @FromJson void pointFromJson(String point) {} } /** * Simple adapter methods are not invoked for null values unless they're annotated * {@code @Nullable}. (The specific annotation class doesn't matter; just its simple name.) */ @Test public void toAndFromNullNotNullable() throws Exception { Moshi moshi = new Moshi.Builder().add(new NotNullablePointAsListOfIntegersJsonAdapter()).build(); JsonAdapter pointAdapter = moshi.adapter(Point.class).lenient(); assertThat(pointAdapter.toJson(null)).isEqualTo("null"); assertThat(pointAdapter.fromJson("null")).isNull(); } static class NotNullablePointAsListOfIntegersJsonAdapter { @ToJson List pointToJson(Point point) { throw new AssertionError(); } @FromJson Point pointFromJson(List o) throws Exception { throw new AssertionError(); } } @Test public void toAndFromNullNullable() throws Exception { Moshi moshi = new Moshi.Builder().add(new NullablePointAsListOfIntegersJsonAdapter()).build(); JsonAdapter pointAdapter = moshi.adapter(Point.class).lenient(); assertThat(pointAdapter.toJson(null)).isEqualTo("[0,0]"); assertThat(pointAdapter.fromJson("null")).isEqualTo(new Point(0, 0)); } static class NullablePointAsListOfIntegersJsonAdapter { @ToJson List pointToJson(@Nullable Point point) { return point != null ? Arrays.asList(point.x, point.y) : Arrays.asList(0, 0); } @FromJson Point pointFromJson(@Nullable List o) throws Exception { if (o == null) return new Point(0, 0); if (o.size() == 2) return new Point(o.get(0), o.get(1)); throw new Exception("Expected null or 2 elements but was " + o); } } @Retention(RetentionPolicy.RUNTIME) @interface Nullable {} @Test public void toAndFromNullJsonWithWriterAndReader() throws Exception { Moshi moshi = new Moshi.Builder().add(new NullableIntToJsonAdapter()).build(); JsonAdapter pointAdapter = moshi.adapter(Point.class); assertThat(pointAdapter.fromJson("{\"x\":null,\"y\":3}")).isEqualTo(new Point(-1, 3)); assertThat(pointAdapter.toJson(new Point(-1, 3))).isEqualTo("{\"y\":3}"); } static class NullableIntToJsonAdapter { @FromJson int jsonToInt(JsonReader reader) throws IOException { if (reader.peek() == JsonReader.Token.NULL) { reader.nextNull(); return -1; } return reader.nextInt(); } @ToJson void intToJson(JsonWriter writer, int value) throws IOException { if (value == -1) { writer.nullValue(); } else { writer.value(value); } } } @Test public void adapterThrows() throws Exception { Moshi moshi = new Moshi.Builder().add(new ExceptionThrowingPointJsonAdapter()).build(); JsonAdapter arrayOfPointAdapter = moshi.adapter(Point[].class).lenient(); try { arrayOfPointAdapter.toJson(new Point[] {null, null, new Point(0, 0)}); fail(); } catch (JsonDataException expected) { assertThat(expected.getMessage()).isEqualTo("java.lang.Exception: pointToJson fail! at $[2]"); } try { arrayOfPointAdapter.fromJson("[null,null,[0,0]]"); fail(); } catch (JsonDataException expected) { assertThat(expected.getMessage()) .isEqualTo("java.lang.Exception: pointFromJson fail! at $[2]"); } } static class ExceptionThrowingPointJsonAdapter { @ToJson void pointToJson(JsonWriter writer, Point point) throws Exception { if (point != null) throw new Exception("pointToJson fail!"); writer.nullValue(); } @FromJson Point pointFromJson(JsonReader reader) throws Exception { if (reader.peek() == JsonReader.Token.NULL) return reader.nextNull(); throw new Exception("pointFromJson fail!"); } } @Test public void adapterDoesToJsonOnly() throws Exception { Object shapeToJsonAdapter = new Object() { @ToJson String shapeToJson(Shape shape) { throw new AssertionError(); } }; Moshi toJsonMoshi = new Moshi.Builder().add(shapeToJsonAdapter).build(); try { toJsonMoshi.adapter(Shape.class); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo( "No @FromJson adapter for interface " + "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e.getCause()) .hasMessageThat() .isEqualTo( "No next JsonAdapter for interface " + "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)"); } } @Test public void adapterDoesFromJsonOnly() throws Exception { Object shapeFromJsonAdapter = new Object() { @FromJson Shape shapeFromJson(String shape) { throw new AssertionError(); } }; Moshi fromJsonMoshi = new Moshi.Builder().add(shapeFromJsonAdapter).build(); try { fromJsonMoshi.adapter(Shape.class); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo( "No @ToJson adapter for interface " + "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e.getCause()) .hasMessageThat() .isEqualTo( "No next JsonAdapter for interface " + "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)"); } } /** * Unfortunately in some versions of Android the implementations of {@link ParameterizedType} * doesn't implement equals and hashCode. Confirm that we work around that. */ @Test public void parameterizedTypeEqualsNotUsed() throws Exception { Moshi moshi = new Moshi.Builder().add(new ListOfStringJsonAdapter()).build(); // This class doesn't implement equals() and hashCode() as it should. ParameterizedType listOfStringType = brokenParameterizedType(0, List.class, String.class); JsonAdapter> jsonAdapter = moshi.adapter(listOfStringType); assertThat(jsonAdapter.toJson(Arrays.asList("a", "b", "c"))).isEqualTo("\"a|b|c\""); assertThat(jsonAdapter.fromJson("\"a|b|c\"")).isEqualTo(Arrays.asList("a", "b", "c")); } static class ListOfStringJsonAdapter { @ToJson String listOfStringToJson(List list) { StringBuilder result = new StringBuilder(); for (int i = 0; i < list.size(); i++) { if (i > 0) result.append('|'); result.append(list.get(i)); } return result.toString(); } @FromJson List listOfStringFromJson(String string) { return Arrays.asList(string.split("\\|")); } } /** * Even when the types we use to look up JSON adapters are not equal, if they're equivalent they * should return the same JsonAdapter instance. */ @Test public void parameterizedTypeCacheKey() throws Exception { Moshi moshi = new Moshi.Builder().build(); Type a = brokenParameterizedType(0, List.class, String.class); Type b = brokenParameterizedType(1, List.class, String.class); Type c = brokenParameterizedType(2, List.class, String.class); assertThat(moshi.adapter(b)).isSameInstanceAs(moshi.adapter(a)); assertThat(moshi.adapter(c)).isSameInstanceAs(moshi.adapter(a)); } @Test public void writerAndReaderTakingJsonAdapterParameter() throws Exception { Moshi moshi = new Moshi.Builder() .add(new PointWriterAndReaderJsonAdapter()) .add(new JsonAdapterWithWriterAndReaderTakingJsonAdapterParameter()) .build(); JsonAdapter lineAdapter = moshi.adapter(Line.class); Line line = new Line(new Point(5, 8), new Point(3, 2)); assertThat(lineAdapter.toJson(line)).isEqualTo("[[5,8],[3,2]]"); assertThat(lineAdapter.fromJson("[[5,8],[3,2]]")).isEqualTo(line); } static class JsonAdapterWithWriterAndReaderTakingJsonAdapterParameter { @ToJson void lineToJson(JsonWriter writer, Line line, JsonAdapter pointAdapter) throws IOException { writer.beginArray(); pointAdapter.toJson(writer, line.a); pointAdapter.toJson(writer, line.b); writer.endArray(); } @FromJson Line lineFromJson(JsonReader reader, JsonAdapter pointAdapter) throws Exception { reader.beginArray(); Point a = pointAdapter.fromJson(reader); Point b = pointAdapter.fromJson(reader); reader.endArray(); return new Line(a, b); } } @Test public void writerAndReaderTakingAnnotatedJsonAdapterParameter() throws Exception { Moshi moshi = new Moshi.Builder() .add(new PointWithParensJsonAdapter()) .add(new JsonAdapterWithWriterAndReaderTakingAnnotatedJsonAdapterParameter()) .build(); JsonAdapter lineAdapter = moshi.adapter(Line.class); Line line = new Line(new Point(5, 8), new Point(3, 2)); assertThat(lineAdapter.toJson(line)).isEqualTo("[\"(5 8)\",\"(3 2)\"]"); assertThat(lineAdapter.fromJson("[\"(5 8)\",\"(3 2)\"]")).isEqualTo(line); } static class PointWithParensJsonAdapter { @ToJson String pointToJson(@WithParens Point point) throws IOException { return String.format("(%s %s)", point.x, point.y); } @FromJson @WithParens Point pointFromJson(String string) throws Exception { Matcher matcher = Pattern.compile("\\((\\d+) (\\d+)\\)").matcher(string); if (!matcher.matches()) throw new JsonDataException(); return new Point(Integer.parseInt(matcher.group(1)), Integer.parseInt(matcher.group(2))); } } static class JsonAdapterWithWriterAndReaderTakingAnnotatedJsonAdapterParameter { @ToJson void lineToJson(JsonWriter writer, Line line, @WithParens JsonAdapter pointAdapter) throws IOException { writer.beginArray(); pointAdapter.toJson(writer, line.a); pointAdapter.toJson(writer, line.b); writer.endArray(); } @FromJson Line lineFromJson(JsonReader reader, @WithParens JsonAdapter pointAdapter) throws Exception { reader.beginArray(); Point a = pointAdapter.fromJson(reader); Point b = pointAdapter.fromJson(reader); reader.endArray(); return new Line(a, b); } } @Test public void writerAndReaderTakingMultipleJsonAdapterParameters() throws Exception { Moshi moshi = new Moshi.Builder() .add(new PointWriterAndReaderJsonAdapter()) .add(new PointWithParensJsonAdapter()) .add(new JsonAdapterWithWriterAndReaderTakingMultipleJsonAdapterParameters()) .build(); JsonAdapter lineAdapter = moshi.adapter(Line.class); Line line = new Line(new Point(5, 8), new Point(3, 2)); assertThat(lineAdapter.toJson(line)).isEqualTo("[[5,8],\"(3 2)\"]"); assertThat(lineAdapter.fromJson("[[5,8],\"(3 2)\"]")).isEqualTo(line); } static class JsonAdapterWithWriterAndReaderTakingMultipleJsonAdapterParameters { @ToJson void lineToJson( JsonWriter writer, Line line, JsonAdapter aAdapter, @WithParens JsonAdapter bAdapter) throws IOException { writer.beginArray(); aAdapter.toJson(writer, line.a); bAdapter.toJson(writer, line.b); writer.endArray(); } @FromJson Line lineFromJson( JsonReader reader, JsonAdapter aAdapter, @WithParens JsonAdapter bAdapter) throws Exception { reader.beginArray(); Point a = aAdapter.fromJson(reader); Point b = bAdapter.fromJson(reader); reader.endArray(); return new Line(a, b); } } @Retention(RUNTIME) @JsonQualifier public @interface WithParens {} @Test public void noToJsonAdapterTakingJsonAdapterParameter() throws Exception { try { new Moshi.Builder().add(new ToJsonAdapterTakingJsonAdapterParameter()); fail(); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessageThat().startsWith("Unexpected signature"); } } static class ToJsonAdapterTakingJsonAdapterParameter { @ToJson String lineToJson(Line line, JsonAdapter pointAdapter) throws IOException { throw new AssertionError(); } } @Test public void noFromJsonAdapterTakingJsonAdapterParameter() throws Exception { try { new Moshi.Builder().add(new FromJsonAdapterTakingJsonAdapterParameter()); fail(); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessageThat().startsWith("Unexpected signature"); } } static class FromJsonAdapterTakingJsonAdapterParameter { @FromJson Line lineFromJson(String value, JsonAdapter pointAdapter) throws Exception { throw new AssertionError(); } } @Test public void adaptedTypeIsEnclosedParameterizedType() throws Exception { Moshi moshi = new Moshi.Builder().add(new EnclosedParameterizedTypeJsonAdapter()).build(); JsonAdapter> boxAdapter = moshi.adapter( Types.newParameterizedTypeWithOwner(AdapterMethodsTest.class, Box.class, Point.class)); Box box = new Box<>(new Point(5, 8)); String json = "[{\"x\":5,\"y\":8}]"; assertThat(boxAdapter.toJson(box)).isEqualTo(json); assertThat(boxAdapter.fromJson(json)).isEqualTo(box); } static class EnclosedParameterizedTypeJsonAdapter { @FromJson Box boxFromJson(List points) { return new Box<>(points.get(0)); } @ToJson List boxToJson(Box box) throws Exception { return Collections.singletonList(box.data); } } static class Box { final T data; public Box(T data) { this.data = data; } @Override public boolean equals(Object o) { return o instanceof Box && ((Box) o).data.equals(data); } @Override public int hashCode() { return data.hashCode(); } } @Test public void genericArrayTypes() throws Exception { Moshi moshi = new Moshi.Builder().add(new ByteArrayJsonAdapter()).build(); JsonAdapter jsonAdapter = moshi.adapter(MapOfByteArrays.class); MapOfByteArrays mapOfByteArrays = new MapOfByteArrays(Collections.singletonMap("a", new byte[] {0, -1})); String json = "{\"map\":{\"a\":\"00ff\"}}"; assertThat(jsonAdapter.toJson(mapOfByteArrays)).isEqualTo(json); assertThat(jsonAdapter.fromJson(json)).isEqualTo(mapOfByteArrays); } static class ByteArrayJsonAdapter { @ToJson String byteArrayToJson(byte[] b) { return ByteString.of(b).hex(); } @FromJson byte[] byteArrayFromJson(String s) throws Exception { return ByteString.decodeHex(s).toByteArray(); } } static class MapOfByteArrays { final Map map; public MapOfByteArrays(Map map) { this.map = map; } @Override public boolean equals(Object o) { return o instanceof MapOfByteArrays && o.toString().equals(toString()); } @Override public int hashCode() { return toString().hashCode(); } @Override public String toString() { StringBuilder result = new StringBuilder(); for (Map.Entry entry : map.entrySet()) { if (result.length() > 0) result.append(", "); result.append(entry.getKey()).append(":").append(Arrays.toString(entry.getValue())); } return result.toString(); } } static class Point { final int x; final int y; public Point(int x, int y) { this.x = x; this.y = y; } @Override public boolean equals(Object o) { return o instanceof Point && ((Point) o).x == x && ((Point) o).y == y; } @Override public int hashCode() { return x * 37 + y; } } static class Line { final Point a; final Point b; public Line(Point a, Point b) { this.a = a; this.b = b; } @Override public boolean equals(Object o) { return o instanceof Line && ((Line) o).a.equals(a) && ((Line) o).b.equals(b); } @Override public int hashCode() { return a.hashCode() * 37 + b.hashCode(); } } interface Shape { String draw(); } /** * Returns a new parameterized type that doesn't implement {@link Object#equals} or {@link * Object#hashCode} by value. These implementation defects are consistent with the parameterized * type that shipped in some older versions of Android. */ ParameterizedType brokenParameterizedType( final int hashCode, final Class rawType, final Type... typeArguments) { return new ParameterizedType() { @Override public Type[] getActualTypeArguments() { return typeArguments; } @Override public Type getRawType() { return rawType; } @Override public Type getOwnerType() { return null; } @Override public boolean equals(Object other) { return other == this; } @Override public int hashCode() { return hashCode; } }; } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/CircularAdaptersTest.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static java.lang.annotation.RetentionPolicy.RUNTIME; import com.squareup.moshi.internal.Util; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.reflect.Type; import java.util.Set; import org.junit.Test; public final class CircularAdaptersTest { static class Team { final String lead; final Project[] projects; public Team(String lead, Project... projects) { this.lead = lead; this.projects = projects; } } static class Project { final String name; final Team[] teams; Project(String name, Team... teams) { this.name = name; this.teams = teams; } } @Test public void circularAdapters() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter teamAdapter = moshi.adapter(Team.class); Team team = new Team( "Alice", new Project("King", new Team("Charlie", new Project("Delivery", (Team[]) null)))); assertThat(teamAdapter.toJson(team)) .isEqualTo( "{\"lead\":\"Alice\",\"projects\":[{\"name\":" + "\"King\",\"teams\":[{\"lead\":\"Charlie\",\"projects\":[{\"name\":\"Delivery\"}]}]}]}"); Team fromJson = teamAdapter.fromJson( "{\"lead\":\"Alice\",\"projects\":[{\"name\":" + "\"King\",\"teams\":[{\"lead\":\"Charlie\",\"projects\":[{\"name\":\"Delivery\"}]}]}]}"); assertThat(fromJson.lead).isEqualTo("Alice"); assertThat(fromJson.projects[0].name).isEqualTo("King"); assertThat(fromJson.projects[0].teams[0].lead).isEqualTo("Charlie"); assertThat(fromJson.projects[0].teams[0].projects[0].name).isEqualTo("Delivery"); } @Retention(RUNTIME) @JsonQualifier public @interface Left {} @Retention(RUNTIME) @JsonQualifier public @interface Right {} static class Node { final String name; final @Left Node left; final @Right Node right; Node(String name, Node left, Node right) { this.name = name; this.left = left; this.right = right; } Node plusPrefix(String prefix) { return new Node(prefix + name, left, right); } Node minusPrefix(String prefix) { if (!name.startsWith(prefix)) throw new IllegalArgumentException(); return new Node(name.substring(prefix.length()), left, right); } } /** * This factory uses extensive delegation. Each node delegates to this for the left and right * subtrees, and those delegate to the built-in class adapter to do most of the serialization * work. */ static class PrefixingNodeFactory implements JsonAdapter.Factory { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { if (type != Node.class) return null; final String prefix; if (Util.isAnnotationPresent(annotations, Left.class)) { prefix = "L "; } else if (Util.isAnnotationPresent(annotations, Right.class)) { prefix = "R "; } else { return null; } final JsonAdapter delegate = moshi.nextAdapter(this, Node.class, Util.NO_ANNOTATIONS); return new JsonAdapter() { @Override public void toJson(JsonWriter writer, Node value) throws IOException { delegate.toJson(writer, value.plusPrefix(prefix)); } @Override public Node fromJson(JsonReader reader) throws IOException { Node result = delegate.fromJson(reader); return result.minusPrefix(prefix); } }.nullSafe(); } } @Test public void circularAdaptersAndAnnotations() throws Exception { Moshi moshi = new Moshi.Builder().add(new PrefixingNodeFactory()).build(); JsonAdapter nodeAdapter = moshi.adapter(Node.class); Node tree = new Node( "C", new Node("A", null, new Node("B", null, null)), new Node("D", null, new Node("E", null, null))); assertThat(nodeAdapter.toJson(tree)) .isEqualTo( "{" + "\"left\":{\"name\":\"L A\",\"right\":{\"name\":\"R B\"}}," + "\"name\":\"C\"," + "\"right\":{\"name\":\"R D\",\"right\":{\"name\":\"R E\"}}" + "}"); Node fromJson = nodeAdapter.fromJson( "{" + "\"left\":{\"name\":\"L A\",\"right\":{\"name\":\"R B\"}}," + "\"name\":\"C\"," + "\"right\":{\"name\":\"R D\",\"right\":{\"name\":\"R E\"}}" + "}"); assertThat(fromJson.name).isEqualTo("C"); assertThat(fromJson.left.name).isEqualTo("A"); assertThat(fromJson.left.right.name).isEqualTo("B"); assertThat(fromJson.right.name).isEqualTo("D"); assertThat(fromJson.right.right.name).isEqualTo("E"); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/DeferredAdapterTest.java ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import javax.annotation.Nullable; import org.junit.Test; public final class DeferredAdapterTest { /** * When a type's JsonAdapter is circularly-dependent, Moshi creates a 'deferred adapter' to make * the cycle work. It's important that any adapters that depend on this deferred adapter don't * leak out until it's ready. * *

This test sets up a circular dependency [BlueNode -> GreenNode -> BlueNode] and then tries * to use a GreenNode JSON adapter before the BlueNode JSON adapter is built. It creates a similar * cycle [BlueNode -> RedNode -> BlueNode] so the order adapters are retrieved is insignificant. * *

This used to trigger a crash because we'd incorrectly put the GreenNode JSON adapter in the * cache even though it depended upon an incomplete BlueNode JSON adapter. */ @Test public void concurrentSafe() { final List failures = new ArrayList<>(); JsonAdapter.Factory factory = new JsonAdapter.Factory() { int redAndGreenCount = 0; @Override public @Nullable JsonAdapter create( Type type, Set annotations, final Moshi moshi) { if ((type == RedNode.class || type == GreenNode.class) && redAndGreenCount++ == 1) { doInAnotherThread( new Runnable() { @Override public void run() { GreenNode greenBlue = new GreenNode(new BlueNode(null, null)); assertThat(moshi.adapter(GreenNode.class).toJson(greenBlue)) .isEqualTo("{\"blue\":{}}"); RedNode redBlue = new RedNode(new BlueNode(null, null)); assertThat(moshi.adapter(RedNode.class).toJson(redBlue)) .isEqualTo("{\"blue\":{}}"); } }); } return null; } }; Moshi moshi = new Moshi.Builder().add(factory).build(); JsonAdapter jsonAdapter = moshi.adapter(BlueNode.class); assertThat(jsonAdapter.toJson(new BlueNode(new GreenNode(new BlueNode(null, null)), null))) .isEqualTo("{\"green\":{\"blue\":{}}}"); assertThat(failures).isEmpty(); } private void doInAnotherThread(Runnable runnable) { ExecutorService executor = Executors.newSingleThreadExecutor(); Future future = executor.submit(runnable); executor.shutdown(); try { future.get(); } catch (InterruptedException e) { throw new RuntimeException(e); } catch (ExecutionException e) { throw new RuntimeException(e.getCause()); } } static class BlueNode { @Nullable GreenNode green; @Nullable RedNode red; BlueNode(@Nullable GreenNode green, @Nullable RedNode red) { this.green = green; this.red = red; } } static class RedNode { @Nullable BlueNode blue; RedNode(@Nullable BlueNode blue) { this.blue = blue; } } static class GreenNode { @Nullable BlueNode blue; GreenNode(@Nullable BlueNode blue) { this.blue = blue; } } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/FlattenTest.java ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import java.io.IOException; import java.util.Arrays; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; /** Note that this test makes heavy use of nested blocks, but these are for readability only. */ @RunWith(Parameterized.class) public final class FlattenTest { @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { return JsonCodecFactory.factories(); } @Test public void flattenExample() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter> integersAdapter = moshi.adapter(Types.newParameterizedType(List.class, Integer.class)); JsonWriter writer = factory.newWriter(); writer.beginArray(); int token = writer.beginFlatten(); writer.value(1); integersAdapter.toJson(writer, Arrays.asList(2, 3, 4)); writer.value(5); writer.endFlatten(token); writer.endArray(); assertThat(factory.json()).isEqualTo("[1,2,3,4,5]"); } @Test public void flattenObject() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); { writer.name("a"); writer.value("aaa"); int token = writer.beginFlatten(); { writer.beginObject(); { writer.name("b"); writer.value("bbb"); } writer.endObject(); } writer.endFlatten(token); writer.name("c"); writer.value("ccc"); } writer.endObject(); assertThat(factory.json()).isEqualTo("{\"a\":\"aaa\",\"b\":\"bbb\",\"c\":\"ccc\"}"); } @Test public void flattenArray() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginArray(); { writer.value("a"); int token = writer.beginFlatten(); { writer.beginArray(); { writer.value("b"); } writer.endArray(); } writer.endFlatten(token); writer.value("c"); } writer.endArray(); assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\"]"); } @Test public void recursiveFlatten() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginArray(); { writer.value("a"); int token1 = writer.beginFlatten(); { writer.beginArray(); { writer.value("b"); int token2 = writer.beginFlatten(); { writer.beginArray(); { writer.value("c"); } writer.endArray(); } writer.endFlatten(token2); writer.value("d"); } writer.endArray(); } writer.endFlatten(token1); writer.value("e"); } writer.endArray(); assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\",\"d\",\"e\"]"); } @Test public void flattenMultipleNested() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginArray(); { writer.value("a"); int token = writer.beginFlatten(); { writer.beginArray(); { writer.value("b"); } writer.endArray(); writer.beginArray(); { writer.value("c"); } writer.endArray(); } writer.endFlatten(token); writer.value("d"); } writer.endArray(); assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\",\"d\"]"); } @Test public void flattenIsOnlyOneLevelDeep() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginArray(); { writer.value("a"); int token = writer.beginFlatten(); { writer.beginArray(); { writer.value("b"); writer.beginArray(); { writer.value("c"); } writer.endArray(); writer.value("d"); } writer.endArray(); } writer.endFlatten(token); writer.value("e"); } writer.endArray(); assertThat(factory.json()).isEqualTo("[\"a\",\"b\",[\"c\"],\"d\",\"e\"]"); } @Test public void flattenOnlySomeChildren() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginArray(); { writer.value("a"); int token = writer.beginFlatten(); { writer.beginArray(); { writer.value("b"); } writer.endArray(); } writer.endFlatten(token); writer.beginArray(); { writer.value("c"); } writer.endArray(); writer.value("d"); } writer.endArray(); assertThat(factory.json()).isEqualTo("[\"a\",\"b\",[\"c\"],\"d\"]"); } @Test public void multipleCallsToFlattenSameNesting() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginArray(); { writer.value("a"); int token1 = writer.beginFlatten(); { writer.beginArray(); { writer.value("b"); } writer.endArray(); int token2 = writer.beginFlatten(); { writer.beginArray(); { writer.value("c"); } writer.endArray(); } writer.endFlatten(token2); writer.beginArray(); { writer.value("d"); } writer.endArray(); } writer.endFlatten(token1); writer.value("e"); } writer.endArray(); assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\",\"d\",\"e\"]"); } @Test public void deepFlatten() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginArray(); { int token1 = writer.beginFlatten(); { writer.beginArray(); { int token2 = writer.beginFlatten(); { writer.beginArray(); { int token3 = writer.beginFlatten(); { writer.beginArray(); { writer.value("a"); } writer.endArray(); } writer.endFlatten(token3); } writer.endArray(); } writer.endFlatten(token2); } writer.endArray(); } writer.endFlatten(token1); } writer.endArray(); assertThat(factory.json()).isEqualTo("[\"a\"]"); } @Test public void flattenTopLevel() throws IOException { JsonWriter writer = factory.newWriter(); try { writer.beginFlatten(); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessageThat().isEqualTo("Nesting problem."); } } @Test public void flattenDoesNotImpactOtherTypesInObjects() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); { int token = writer.beginFlatten(); writer.name("a"); writer.beginArray(); writer.value("aaa"); writer.endArray(); writer.beginObject(); { writer.name("b"); writer.value("bbb"); } writer.endObject(); writer.name("c"); writer.beginArray(); writer.value("ccc"); writer.endArray(); writer.endFlatten(token); } writer.endObject(); assertThat(factory.json()).isEqualTo("{\"a\":[\"aaa\"],\"b\":\"bbb\",\"c\":[\"ccc\"]}"); } @Test public void flattenDoesNotImpactOtherTypesInArrays() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginArray(); { int token = writer.beginFlatten(); { writer.beginObject(); { writer.name("a"); writer.value("aaa"); } writer.endObject(); writer.beginArray(); { writer.value("bbb"); } writer.endArray(); writer.value("ccc"); writer.beginObject(); { writer.name("d"); writer.value("ddd"); } writer.endObject(); } writer.endFlatten(token); } writer.endArray(); assertThat(factory.json()).isEqualTo("[{\"a\":\"aaa\"},\"bbb\",\"ccc\",{\"d\":\"ddd\"}]"); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonAdapterTest.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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.Map; import javax.annotation.Nullable; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public final class JsonAdapterTest { @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { return JsonCodecFactory.factories(); } @Test public void lenient() throws Exception { JsonAdapter lenient = new JsonAdapter() { @Override public Double fromJson(JsonReader reader) throws IOException { return reader.nextDouble(); } @Override public void toJson(JsonWriter writer, Double value) throws IOException { writer.value(value); } }.lenient(); JsonReader reader = factory.newReader("[-Infinity, NaN, Infinity]"); reader.beginArray(); assertThat(lenient.fromJson(reader)).isEqualTo(Double.NEGATIVE_INFINITY); assertThat(lenient.fromJson(reader)).isNaN(); assertThat(lenient.fromJson(reader)).isEqualTo(Double.POSITIVE_INFINITY); reader.endArray(); JsonWriter writer = factory.newWriter(); writer.beginArray(); lenient.toJson(writer, Double.NEGATIVE_INFINITY); lenient.toJson(writer, Double.NaN); lenient.toJson(writer, Double.POSITIVE_INFINITY); writer.endArray(); assertThat(factory.json()).isEqualTo("[-Infinity,NaN,Infinity]"); } @Test public void nullSafe() throws Exception { JsonAdapter toUpperCase = new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { return reader.nextString().toUpperCase(Locale.US); } @Override public void toJson(JsonWriter writer, String value) throws IOException { writer.value(value.toUpperCase(Locale.US)); } }.nullSafe(); JsonReader reader = factory.newReader("[\"a\", null, \"c\"]"); reader.beginArray(); assertThat(toUpperCase.fromJson(reader)).isEqualTo("A"); assertThat(toUpperCase.fromJson(reader)).isNull(); assertThat(toUpperCase.fromJson(reader)).isEqualTo("C"); reader.endArray(); JsonWriter writer = factory.newWriter(); writer.beginArray(); toUpperCase.toJson(writer, "a"); toUpperCase.toJson(writer, null); toUpperCase.toJson(writer, "c"); writer.endArray(); assertThat(factory.json()).isEqualTo("[\"A\",null,\"C\"]"); } @Test public void nonNull() throws Exception { JsonAdapter toUpperCase = new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { return reader.nextString().toUpperCase(Locale.US); } @Override public void toJson(JsonWriter writer, String value) throws IOException { writer.value(value.toUpperCase(Locale.US)); } }.nonNull(); JsonReader reader = factory.newReader("[\"a\", null, \"c\"]"); reader.beginArray(); assertThat(toUpperCase.fromJson(reader)).isEqualTo("A"); try { toUpperCase.fromJson(reader); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Unexpected null at $[1]"); assertThat(reader.nextNull()).isNull(); } assertThat(toUpperCase.fromJson(reader)).isEqualTo("C"); reader.endArray(); JsonWriter writer = factory.newWriter(); writer.beginArray(); toUpperCase.toJson(writer, "a"); try { toUpperCase.toJson(writer, null); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Unexpected null at $[1]"); writer.nullValue(); } toUpperCase.toJson(writer, "c"); writer.endArray(); assertThat(factory.json()).isEqualTo("[\"A\",null,\"C\"]"); } @Test public void failOnUnknown() throws Exception { JsonAdapter alwaysSkip = new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { reader.skipValue(); throw new AssertionError(); } @Override public void toJson(JsonWriter writer, String value) throws IOException { throw new AssertionError(); } }.failOnUnknown(); JsonReader reader = factory.newReader("[\"a\"]"); reader.beginArray(); try { alwaysSkip.fromJson(reader); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Cannot skip unexpected STRING at $[0]"); } assertThat(reader.nextString()).isEqualTo("a"); reader.endArray(); } @Test public void indent() throws Exception { assumeTrue(factory.encodesToBytes()); JsonAdapter> indent = new JsonAdapter>() { @Override public List fromJson(JsonReader reader) throws IOException { throw new AssertionError(); } @Override public void toJson(JsonWriter writer, List value) throws IOException { writer.beginArray(); for (String s : value) { writer.value(s); } writer.endArray(); } }.indent("\t\t\t"); JsonWriter writer = factory.newWriter(); indent.toJson(writer, Arrays.asList("a", "b", "c")); assertThat(factory.json()) .isEqualTo("" + "[\n" + "\t\t\t\"a\",\n" + "\t\t\t\"b\",\n" + "\t\t\t\"c\"\n" + "]"); } @Test public void indentDisallowsNull() throws Exception { JsonAdapter adapter = new JsonAdapter() { @Override public Object fromJson(JsonReader reader) { throw new AssertionError(); } @Override public void toJson(JsonWriter writer, Object value) { throw new AssertionError(); } }; try { adapter.indent(null); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } } @Test public void serializeNulls() throws Exception { JsonAdapter> serializeNulls = new JsonAdapter>() { @Override public Map fromJson(JsonReader reader) throws IOException { throw new AssertionError(); } @Override public void toJson(JsonWriter writer, Map map) throws IOException { writer.beginObject(); for (Map.Entry entry : map.entrySet()) { writer.name(entry.getKey()).value(entry.getValue()); } writer.endObject(); } }.serializeNulls(); JsonWriter writer = factory.newWriter(); serializeNulls.toJson(writer, Collections.singletonMap("a", null)); assertThat(factory.json()).isEqualTo("{\"a\":null}"); } @Test public void stringDocumentMustBeFullyConsumed() throws IOException { JsonAdapter brokenAdapter = new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { return "Forgot to call reader.nextString()."; } @Override public void toJson(JsonWriter writer, @Nullable String value) throws IOException { throw new AssertionError(); } }; try { brokenAdapter.fromJson("\"value\""); fail(); } catch (JsonDataException e) { assertThat(e).hasMessageThat().isEqualTo("JSON document was not fully consumed."); } } @Test public void adapterFromJsonStringPeeksAtEnd() throws IOException { JsonAdapter adapter = new JsonAdapter() { @Override public Boolean fromJson(JsonReader reader) throws IOException { return reader.nextBoolean(); } @Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException { throw new AssertionError(); } }; try { adapter.fromJson("true true"); fail(); } catch (JsonEncodingException e) { assertThat(e) .hasMessageThat() .isEqualTo("Use JsonReader.setLenient(true) to accept malformed JSON at path $"); } } @Test public void lenientAdapterFromJsonStringDoesNotPeekAtEnd() throws IOException { JsonAdapter adapter = new JsonAdapter() { @Override public Boolean fromJson(JsonReader reader) throws IOException { return reader.nextBoolean(); } @Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException { throw new AssertionError(); } }.lenient(); assertThat(adapter.fromJson("true true")).isEqualTo(true); } @Test public void adaptersDelegateLeniency() throws IOException { JsonAdapter adapter = new JsonAdapter() { @Override public Boolean fromJson(JsonReader reader) throws IOException { return reader.nextBoolean(); } @Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException { throw new AssertionError(); } }.lenient().serializeNulls(); assertThat(adapter.fromJson("true true")).isEqualTo(true); } @Test public void nullSafeDoesntDuplicate() { JsonAdapter adapter = new Moshi.Builder().build().adapter(Boolean.class).nullSafe(); assertThat(adapter.nullSafe()).isSameInstanceAs(adapter); } @Test public void nonNullDoesntDuplicate() { JsonAdapter adapter = new Moshi.Builder().build().adapter(Boolean.class).nonNull(); assertThat(adapter.nonNull()).isSameInstanceAs(adapter); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonCodecFactory.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 * * https://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.squareup.moshi; import static com.squareup.moshi.MoshiTesting.jsonValueReader; import java.io.IOException; import java.util.Arrays; import java.util.List; import okio.Buffer; import okio.BufferedSource; abstract class JsonCodecFactory { private static final Moshi MOSHI = new Moshi.Builder().build(); private static final JsonAdapter OBJECT_ADAPTER = MOSHI.adapter(Object.class); static List factories() { final JsonCodecFactory utf8 = new JsonCodecFactory() { Buffer buffer; @Override public JsonReader newReader(String json) { Buffer buffer = new Buffer().writeUtf8(json); return JsonReader.of(buffer); } @Override JsonWriter newWriter() { buffer = new Buffer(); return JsonWriter.of(buffer); } @Override String json() { String result = buffer.readUtf8(); buffer = null; return result; } @Override boolean encodesToBytes() { return true; } @Override public String toString() { return "Utf8"; } }; final JsonCodecFactory value = new JsonCodecFactory() { JsonWriter writer; @Override public JsonReader newReader(String json) throws IOException { Moshi moshi = new Moshi.Builder().build(); Object object = moshi.adapter(Object.class).lenient().fromJson(json); return jsonValueReader(object); } // TODO(jwilson): fix precision checks and delete his method. @Override boolean implementsStrictPrecision() { return false; } @Override JsonWriter newWriter() { writer = MoshiTesting.jsonValueWriter(); return writer; } @Override String json() { // This writer writes a DOM. Use other Moshi features to serialize it as a string. try { Buffer buffer = new Buffer(); JsonWriter bufferedSinkWriter = JsonWriter.of(buffer); bufferedSinkWriter.setSerializeNulls(true); bufferedSinkWriter.setLenient(true); OBJECT_ADAPTER.toJson(bufferedSinkWriter, MoshiTesting.root(writer)); return buffer.readUtf8(); } catch (IOException e) { throw new AssertionError(); } } // TODO(jwilson): support BigDecimal and BigInteger and delete his method. @Override boolean supportsBigNumbers() { return false; } @Override public String toString() { return "Value"; } }; final JsonCodecFactory valuePeek = new JsonCodecFactory() { @Override public JsonReader newReader(String json) throws IOException { return value.newReader(json).peekJson(); } // TODO(jwilson): fix precision checks and delete his method. @Override boolean implementsStrictPrecision() { return false; } @Override JsonWriter newWriter() throws IOException { return value.newWriter(); } @Override String json() throws IOException { return value.json(); } // TODO(jwilson): support BigDecimal and BigInteger and delete his method. @Override boolean supportsBigNumbers() { return false; } @Override public String toString() { return "ValuePeek"; } }; /** Wrap the enclosing JsonReader or JsonWriter in another stream. */ final JsonCodecFactory valueSource = new JsonCodecFactory() { private JsonWriter writer; @Override public JsonReader newReader(String json) throws IOException { JsonReader wrapped = utf8.newReader(json); wrapped.setLenient(true); BufferedSource valueSource = wrapped.nextSource(); return JsonReader.of(valueSource); } @Override JsonWriter newWriter() throws IOException { JsonWriter wrapped = utf8.newWriter(); wrapped.setLenient(true); writer = JsonWriter.of(wrapped.valueSink()); return writer; } @Override String json() throws IOException { writer.close(); return utf8.json(); } @Override public String toString() { return "ValueSource"; } }; return Arrays.asList( new Object[] {utf8}, new Object[] {value}, new Object[] {valuePeek}, new Object[] {valueSource}); } abstract JsonReader newReader(String json) throws IOException; abstract JsonWriter newWriter() throws IOException; boolean implementsStrictPrecision() { return true; } abstract String json() throws IOException; boolean encodesToBytes() { return false; } boolean supportsBigNumbers() { return true; } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonQualifiersTest.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.Assert.fail; import java.io.IOException; import java.lang.annotation.Retention; import java.util.Date; import org.junit.Test; public final class JsonQualifiersTest { @Test public void builtInTypes() throws Exception { Moshi moshi = new Moshi.Builder().add(new BuiltInTypesJsonAdapter()).build(); JsonAdapter adapter = moshi.adapter(StringAndFooString.class); StringAndFooString v1 = new StringAndFooString(); v1.a = "aa"; v1.b = "bar"; assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobar\"}"); StringAndFooString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobar\"}"); assertThat(v2.a).isEqualTo("aa"); assertThat(v2.b).isEqualTo("bar"); } static class BuiltInTypesJsonAdapter { @ToJson String fooPrefixStringToString(@FooPrefix String s) { return "foo" + s; } @FromJson @FooPrefix String fooPrefixStringFromString(String s) throws Exception { if (!s.startsWith("foo")) throw new JsonDataException(); return s.substring(3); } } @Test public void readerWriterJsonAdapter() throws Exception { Moshi moshi = new Moshi.Builder().add(new ReaderWriterJsonAdapter()).build(); JsonAdapter adapter = moshi.adapter(StringAndFooString.class); StringAndFooString v1 = new StringAndFooString(); v1.a = "aa"; v1.b = "bar"; assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobar\"}"); StringAndFooString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobar\"}"); assertThat(v2.a).isEqualTo("aa"); assertThat(v2.b).isEqualTo("bar"); } static class ReaderWriterJsonAdapter { @ToJson void fooPrefixStringToString(JsonWriter jsonWriter, @FooPrefix String s) throws IOException { jsonWriter.value("foo" + s); } @FromJson @FooPrefix String fooPrefixStringFromString(JsonReader reader) throws Exception { String s = reader.nextString(); if (!s.startsWith("foo")) throw new JsonDataException(); return s.substring(3); } } /** Fields with this annotation get "foo" as a prefix in the JSON. */ @Retention(RUNTIME) @JsonQualifier public @interface FooPrefix {} /** Fields with this annotation get "baz" as a suffix in the JSON. */ @Retention(RUNTIME) @JsonQualifier public @interface BazSuffix {} static class StringAndFooString { String a; @FooPrefix String b; } static class StringAndFooBazString { String a; @FooPrefix @BazSuffix String b; } @Test public void builtInTypesWithMultipleAnnotations() throws Exception { Moshi moshi = new Moshi.Builder().add(new BuiltInTypesWithMultipleAnnotationsJsonAdapter()).build(); JsonAdapter adapter = moshi.adapter(StringAndFooBazString.class); StringAndFooBazString v1 = new StringAndFooBazString(); v1.a = "aa"; v1.b = "bar"; assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); StringAndFooBazString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); assertThat(v2.a).isEqualTo("aa"); assertThat(v2.b).isEqualTo("bar"); } static class BuiltInTypesWithMultipleAnnotationsJsonAdapter { @ToJson String fooPrefixAndBazSuffixStringToString(@FooPrefix @BazSuffix String s) { return "foo" + s + "baz"; } @FromJson @FooPrefix @BazSuffix String fooPrefixAndBazSuffixStringFromString(String s) throws Exception { if (!s.startsWith("foo")) throw new JsonDataException(); if (!s.endsWith("baz")) throw new JsonDataException(); return s.substring(3, s.length() - 3); } } @Test public void readerWriterWithMultipleAnnotations() throws Exception { Moshi moshi = new Moshi.Builder().add(new ReaderWriterWithMultipleAnnotationsJsonAdapter()).build(); JsonAdapter adapter = moshi.adapter(StringAndFooBazString.class); StringAndFooBazString v1 = new StringAndFooBazString(); v1.a = "aa"; v1.b = "bar"; assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); StringAndFooBazString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); assertThat(v2.a).isEqualTo("aa"); assertThat(v2.b).isEqualTo("bar"); } static class ReaderWriterWithMultipleAnnotationsJsonAdapter { @ToJson void fooPrefixAndBazSuffixStringToString(JsonWriter jsonWriter, @FooPrefix @BazSuffix String s) throws IOException { jsonWriter.value("foo" + s + "baz"); } @FromJson @FooPrefix @BazSuffix String fooPrefixAndBazSuffixStringFromString(JsonReader reader) throws Exception { String s = reader.nextString(); if (!s.startsWith("foo")) throw new JsonDataException(); if (!s.endsWith("baz")) throw new JsonDataException(); return s.substring(3, s.length() - 3); } } @Test public void basicTypesAnnotationDelegating() throws Exception { Moshi moshi = new Moshi.Builder() .add(new BuiltInTypesDelegatingJsonAdapter()) .add(new BuiltInTypesJsonAdapter()) .build(); JsonAdapter adapter = moshi.adapter(StringAndFooBazString.class); StringAndFooBazString v1 = new StringAndFooBazString(); v1.a = "aa"; v1.b = "bar"; assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); StringAndFooBazString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); assertThat(v2.a).isEqualTo("aa"); assertThat(v2.b).isEqualTo("bar"); } static class BuiltInTypesDelegatingJsonAdapter { @ToJson @FooPrefix String fooPrefixAndBazSuffixStringToString(@FooPrefix @BazSuffix String s) { return s + "baz"; } @FromJson @FooPrefix @BazSuffix String fooPrefixAndBazSuffixStringFromString(@FooPrefix String s) throws Exception { if (!s.endsWith("baz")) throw new JsonDataException(); return s.substring(0, s.length() - 3); } } @Test public void readerWriterAnnotationDelegating() throws Exception { Moshi moshi = new Moshi.Builder() .add(new BuiltInTypesDelegatingJsonAdapter()) .add(new ReaderWriterJsonAdapter()) .build(); JsonAdapter adapter = moshi.adapter(StringAndFooBazString.class); StringAndFooBazString v1 = new StringAndFooBazString(); v1.a = "aa"; v1.b = "bar"; assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); StringAndFooBazString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobarbaz\"}"); assertThat(v2.a).isEqualTo("aa"); assertThat(v2.b).isEqualTo("bar"); } @Test public void manualJsonAdapter() throws Exception { JsonAdapter fooPrefixAdapter = new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { String s = reader.nextString(); if (!s.startsWith("foo")) throw new JsonDataException(); return s.substring(3); } @Override public void toJson(JsonWriter writer, String value) throws IOException { writer.value("foo" + value); } }; Moshi moshi = new Moshi.Builder().add(String.class, FooPrefix.class, fooPrefixAdapter).build(); JsonAdapter adapter = moshi.adapter(StringAndFooString.class); StringAndFooString v1 = new StringAndFooString(); v1.a = "aa"; v1.b = "bar"; assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":\"aa\",\"b\":\"foobar\"}"); StringAndFooString v2 = adapter.fromJson("{\"a\":\"aa\",\"b\":\"foobar\"}"); assertThat(v2.a).isEqualTo("aa"); assertThat(v2.b).isEqualTo("bar"); } @Test public void noJsonAdapterForAnnotatedType() throws Exception { Moshi moshi = new Moshi.Builder().build(); try { moshi.adapter(StringAndFooString.class); fail(); } catch (IllegalArgumentException expected) { } } @Test public void annotationWithoutJsonQualifierIsIgnoredByAdapterMethods() throws Exception { Moshi moshi = new Moshi.Builder().add(new MissingJsonQualifierJsonAdapter()).build(); JsonAdapter adapter = moshi.adapter(DateAndMillisDate.class); DateAndMillisDate v1 = new DateAndMillisDate(); v1.a = new Date(5); v1.b = new Date(7); assertThat(adapter.toJson(v1)).isEqualTo("{\"a\":5,\"b\":7}"); DateAndMillisDate v2 = adapter.fromJson("{\"a\":5,\"b\":7}"); assertThat(v2.a).isEqualTo(new Date(5)); assertThat(v2.b).isEqualTo(new Date(7)); } /** Despite the fact that these methods are annotated, they match all dates. */ static class MissingJsonQualifierJsonAdapter { @ToJson long dateToJson(@Millis Date d) { return d.getTime(); } @FromJson @Millis Date jsonToDate(long value) throws Exception { return new Date(value); } } /** This annotation does nothing. */ @Retention(RUNTIME) public @interface Millis {} static class DateAndMillisDate { Date a; @Millis Date b; } @Test public void annotationWithoutJsonQualifierIsRejectedOnRegistration() throws Exception { JsonAdapter jsonAdapter = new JsonAdapter() { @Override public Date fromJson(JsonReader reader) throws IOException { throw new AssertionError(); } @Override public void toJson(JsonWriter writer, Date value) throws IOException { throw new AssertionError(); } }; try { new Moshi.Builder().add(Date.class, Millis.class, jsonAdapter); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "interface com.squareup.moshi.JsonQualifiersTest$Millis " + "does not have @JsonQualifier"); } } @Test public void annotationsConflict() throws Exception { try { new Moshi.Builder().add(new AnnotationsConflictJsonAdapter()); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessageThat().contains("Conflicting @ToJson methods"); } } static class AnnotationsConflictJsonAdapter { @ToJson String fooPrefixStringToString(@FooPrefix String s) { return "foo" + s; } @ToJson String fooPrefixStringToString2(@FooPrefix String s) { return "foo" + s; } } @Test public void toButNoFromJson() throws Exception { // Building it is okay. Moshi moshi = new Moshi.Builder().add(new ToButNoFromJsonAdapter()).build(); try { moshi.adapter(StringAndFooString.class); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "No @FromJson adapter for class java.lang.String annotated " + "[@com.squareup.moshi.JsonQualifiersTest.FooPrefix()]" + "\nfor class java.lang.String b" + "\nfor class com.squareup.moshi.JsonQualifiersTest$StringAndFooString"); assertThat(expected).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(expected.getCause()) .hasMessageThat() .isEqualTo( "No @FromJson adapter for class java.lang.String " + "annotated [@com.squareup.moshi.JsonQualifiersTest.FooPrefix()]"); assertThat(expected.getCause()).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(expected.getCause().getCause()) .hasMessageThat() .isEqualTo( "No next JsonAdapter for class " + "java.lang.String annotated [@com.squareup.moshi.JsonQualifiersTest.FooPrefix()]"); } } static class ToButNoFromJsonAdapter { @ToJson String fooPrefixStringToString(@FooPrefix String s) { return "foo" + s; } } @Test public void fromButNoToJson() throws Exception { // Building it is okay. Moshi moshi = new Moshi.Builder().add(new FromButNoToJsonAdapter()).build(); try { moshi.adapter(StringAndFooString.class); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "No @ToJson adapter for class java.lang.String annotated " + "[@com.squareup.moshi.JsonQualifiersTest.FooPrefix()]" + "\nfor class java.lang.String b" + "\nfor class com.squareup.moshi.JsonQualifiersTest$StringAndFooString"); assertThat(expected).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(expected.getCause()) .hasMessageThat() .isEqualTo( "No @ToJson adapter for class java.lang.String " + "annotated [@com.squareup.moshi.JsonQualifiersTest.FooPrefix()]"); assertThat(expected.getCause()).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(expected.getCause().getCause()) .hasMessageThat() .isEqualTo( "No next JsonAdapter for class " + "java.lang.String annotated [@com.squareup.moshi.JsonQualifiersTest.FooPrefix()]"); } } static class FromButNoToJsonAdapter { @FromJson @FooPrefix String fooPrefixStringFromString(String s) throws Exception { if (!s.startsWith("foo")) throw new JsonDataException(); return s.substring(3); } } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonReaderPathTest.java ================================================ /* * Copyright (C) 2014 Google 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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeTrue; import java.io.IOException; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public final class JsonReaderPathTest { @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { return JsonCodecFactory.factories(); } @SuppressWarnings("CheckReturnValue") @Test public void path() throws IOException { JsonReader reader = factory.newReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}"); assertThat(reader.getPath()).isEqualTo("$"); reader.beginObject(); assertThat(reader.getPath()).isEqualTo("$."); reader.nextName(); assertThat(reader.getPath()).isEqualTo("$.a"); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$.a[0]"); reader.nextInt(); assertThat(reader.getPath()).isEqualTo("$.a[1]"); reader.nextBoolean(); assertThat(reader.getPath()).isEqualTo("$.a[2]"); reader.nextBoolean(); assertThat(reader.getPath()).isEqualTo("$.a[3]"); reader.nextNull(); assertThat(reader.getPath()).isEqualTo("$.a[4]"); reader.nextString(); assertThat(reader.getPath()).isEqualTo("$.a[5]"); reader.beginObject(); assertThat(reader.getPath()).isEqualTo("$.a[5]."); reader.nextName(); assertThat(reader.getPath()).isEqualTo("$.a[5].c"); reader.nextString(); assertThat(reader.getPath()).isEqualTo("$.a[5].c"); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$.a[6]"); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$.a[6][0]"); reader.nextInt(); assertThat(reader.getPath()).isEqualTo("$.a[6][1]"); reader.endArray(); assertThat(reader.getPath()).isEqualTo("$.a[7]"); reader.endArray(); assertThat(reader.getPath()).isEqualTo("$.a"); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void arrayOfObjects() throws IOException { JsonReader reader = factory.newReader("[{},{},{}]"); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$[0]"); reader.beginObject(); assertThat(reader.getPath()).isEqualTo("$[0]."); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$[1]"); reader.beginObject(); assertThat(reader.getPath()).isEqualTo("$[1]."); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$[2]"); reader.beginObject(); assertThat(reader.getPath()).isEqualTo("$[2]."); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$[3]"); reader.endArray(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void arrayOfArrays() throws IOException { JsonReader reader = factory.newReader("[[],[],[]]"); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$[0]"); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$[0][0]"); reader.endArray(); assertThat(reader.getPath()).isEqualTo("$[1]"); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$[1][0]"); reader.endArray(); assertThat(reader.getPath()).isEqualTo("$[2]"); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$[2][0]"); reader.endArray(); assertThat(reader.getPath()).isEqualTo("$[3]"); reader.endArray(); assertThat(reader.getPath()).isEqualTo("$"); } @SuppressWarnings("CheckReturnValue") @Test public void objectPath() throws IOException { JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}"); assertThat(reader.getPath()).isEqualTo("$"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$"); reader.beginObject(); assertThat(reader.getPath()).isEqualTo("$."); reader.peek(); assertThat(reader.getPath()).isEqualTo("$."); reader.nextName(); assertThat(reader.getPath()).isEqualTo("$.a"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$.a"); reader.nextInt(); assertThat(reader.getPath()).isEqualTo("$.a"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$.a"); reader.nextName(); assertThat(reader.getPath()).isEqualTo("$.b"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$.b"); reader.nextInt(); assertThat(reader.getPath()).isEqualTo("$.b"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$.b"); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$"); reader.close(); assertThat(reader.getPath()).isEqualTo("$"); } @SuppressWarnings("CheckReturnValue") @Test public void arrayPath() throws IOException { JsonReader reader = factory.newReader("[1,2]"); assertThat(reader.getPath()).isEqualTo("$"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$"); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$[0]"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$[0]"); reader.nextInt(); assertThat(reader.getPath()).isEqualTo("$[1]"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$[1]"); reader.nextInt(); assertThat(reader.getPath()).isEqualTo("$[2]"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$[2]"); reader.endArray(); assertThat(reader.getPath()).isEqualTo("$"); reader.peek(); assertThat(reader.getPath()).isEqualTo("$"); reader.close(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void multipleTopLevelValuesInOneDocument() throws IOException { assumeTrue(factory.encodesToBytes()); JsonReader reader = factory.newReader("[][]"); reader.setLenient(true); reader.beginArray(); reader.endArray(); assertThat(reader.getPath()).isEqualTo("$"); reader.beginArray(); reader.endArray(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void skipArrayElements() throws IOException { JsonReader reader = factory.newReader("[1,2,3]"); reader.beginArray(); reader.skipValue(); reader.skipValue(); assertThat(reader.getPath()).isEqualTo("$[2]"); } @Test public void skipObjectNames() throws IOException { JsonReader reader = factory.newReader("{\"a\":1}"); reader.beginObject(); reader.skipValue(); assertThat(reader.getPath()).isEqualTo("$.null"); } @SuppressWarnings("CheckReturnValue") @Test public void skipObjectValues() throws IOException { JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}"); reader.beginObject(); reader.nextName(); reader.skipValue(); assertThat(reader.getPath()).isEqualTo("$.null"); reader.nextName(); assertThat(reader.getPath()).isEqualTo("$.b"); } @Test public void skipNestedStructures() throws IOException { JsonReader reader = factory.newReader("[[1,2,3],4]"); reader.beginArray(); reader.skipValue(); assertThat(reader.getPath()).isEqualTo("$[1]"); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonReaderTest.java ================================================ /* * Copyright (C) 2010 Google 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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static com.squareup.moshi.JsonReader.Token.BEGIN_ARRAY; import static com.squareup.moshi.JsonReader.Token.BEGIN_OBJECT; import static com.squareup.moshi.JsonReader.Token.NAME; import static com.squareup.moshi.JsonReader.Token.STRING; import static com.squareup.moshi.TestUtil.repeat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import java.io.EOFException; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.List; import okio.BufferedSource; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) @SuppressWarnings("CheckReturnValue") public final class JsonReaderTest { @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { return JsonCodecFactory.factories(); } JsonReader newReader(String json) throws IOException { return factory.newReader(json); } @Test public void readArray() throws IOException { JsonReader reader = newReader("[true, true]"); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); assertThat(reader.nextBoolean()).isTrue(); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void readEmptyArray() throws IOException { JsonReader reader = newReader("[]"); reader.beginArray(); assertThat(reader.hasNext()).isFalse(); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void readObject() throws IOException { JsonReader reader = newReader("{\"a\": \"android\", \"b\": \"banana\"}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextString()).isEqualTo("android"); assertThat(reader.nextName()).isEqualTo("b"); assertThat(reader.nextString()).isEqualTo("banana"); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void readEmptyObject() throws IOException { JsonReader reader = newReader("{}"); reader.beginObject(); assertThat(reader.hasNext()).isFalse(); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipArray() throws IOException { JsonReader reader = newReader("{\"a\": [\"one\", \"two\", \"three\"], \"b\": 123}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); reader.skipValue(); assertThat(reader.nextName()).isEqualTo("b"); assertThat(reader.nextInt()).isEqualTo(123); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipArrayAfterPeek() throws Exception { JsonReader reader = newReader("{\"a\": [\"one\", \"two\", \"three\"], \"b\": 123}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.peek()).isEqualTo(BEGIN_ARRAY); reader.skipValue(); assertThat(reader.nextName()).isEqualTo("b"); assertThat(reader.nextInt()).isEqualTo(123); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipTopLevelObject() throws Exception { JsonReader reader = newReader("{\"a\": [\"one\", \"two\", \"three\"], \"b\": 123}"); reader.skipValue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipObject() throws IOException { JsonReader reader = newReader("{\"a\": { \"c\": [], \"d\": [true, true, {}] }, \"b\": \"banana\"}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); reader.skipValue(); assertThat(reader.nextName()).isEqualTo("b"); reader.skipValue(); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipObjectAfterPeek() throws Exception { String json = "{" + " \"one\": { \"num\": 1 }" + ", \"two\": { \"num\": 2 }" + ", \"three\": { \"num\": 3 }" + "}"; JsonReader reader = newReader(json); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("one"); assertThat(reader.peek()).isEqualTo(BEGIN_OBJECT); reader.skipValue(); assertThat(reader.nextName()).isEqualTo("two"); assertThat(reader.peek()).isEqualTo(BEGIN_OBJECT); reader.skipValue(); assertThat(reader.nextName()).isEqualTo("three"); reader.skipValue(); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipInteger() throws IOException { JsonReader reader = newReader("{\"a\":123456789,\"b\":-123456789}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); reader.skipValue(); assertThat(reader.nextName()).isEqualTo("b"); reader.skipValue(); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipDouble() throws IOException { JsonReader reader = newReader("{\"a\":-123.456e-789,\"b\":123456789.0}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); reader.skipValue(); assertThat(reader.nextName()).isEqualTo("b"); reader.skipValue(); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void failOnUnknownFailsOnUnknownObjectValue() throws IOException { JsonReader reader = newReader("{\"a\": 123}"); reader.setFailOnUnknown(true); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { reader.skipValue(); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Cannot skip unexpected NUMBER at $.a"); } // Confirm that the reader is left in a consistent state after the exception. reader.setFailOnUnknown(false); assertThat(reader.nextInt()).isEqualTo(123); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void failOnUnknownFailsOnUnknownArrayElement() throws IOException { JsonReader reader = newReader("[\"a\", 123]"); reader.setFailOnUnknown(true); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("a"); try { reader.skipValue(); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Cannot skip unexpected NUMBER at $[1]"); } // Confirm that the reader is left in a consistent state after the exception. reader.setFailOnUnknown(false); assertThat(reader.nextInt()).isEqualTo(123); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void helloWorld() throws IOException { String json = "{\n" + " \"hello\": true,\n" + " \"foo\": [\"world\"]\n" + "}"; JsonReader reader = newReader(json); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("hello"); assertThat(reader.nextBoolean()).isTrue(); assertThat(reader.nextName()).isEqualTo("foo"); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("world"); reader.endArray(); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void emptyString() throws Exception { try { newReader("").beginArray(); fail(); } catch (EOFException expected) { } try { newReader("").beginObject(); fail(); } catch (EOFException expected) { } } @Test public void characterUnescaping() throws IOException { String json = "[\"a\"," + "\"a\\\"\"," + "\"\\\"\"," + "\":\"," + "\",\"," + "\"\\b\"," + "\"\\f\"," + "\"\\n\"," + "\"\\r\"," + "\"\\t\"," + "\" \"," + "\"\\\\\"," + "\"{\"," + "\"}\"," + "\"[\"," + "\"]\"," + "\"\\u0000\"," + "\"\\u0019\"," + "\"\\u20AC\"" + "]"; JsonReader reader = newReader(json); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("a"); assertThat(reader.nextString()).isEqualTo("a\""); assertThat(reader.nextString()).isEqualTo("\""); assertThat(reader.nextString()).isEqualTo(":"); assertThat(reader.nextString()).isEqualTo(","); assertThat(reader.nextString()).isEqualTo("\b"); assertThat(reader.nextString()).isEqualTo("\f"); assertThat(reader.nextString()).isEqualTo("\n"); assertThat(reader.nextString()).isEqualTo("\r"); assertThat(reader.nextString()).isEqualTo("\t"); assertThat(reader.nextString()).isEqualTo(" "); assertThat(reader.nextString()).isEqualTo("\\"); assertThat(reader.nextString()).isEqualTo("{"); assertThat(reader.nextString()).isEqualTo("}"); assertThat(reader.nextString()).isEqualTo("["); assertThat(reader.nextString()).isEqualTo("]"); assertThat(reader.nextString()).isEqualTo("\0"); assertThat(reader.nextString()).isEqualTo("\u0019"); assertThat(reader.nextString()).isEqualTo("\u20AC"); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void integersWithFractionalPartSpecified() throws IOException { JsonReader reader = newReader("[1.0,1.0,1.0]"); reader.beginArray(); assertThat(reader.nextDouble()).isEqualTo(1.0); assertThat(reader.nextInt()).isEqualTo(1); assertThat(reader.nextLong()).isEqualTo(1L); } @Test public void doubles() throws IOException { String json = "[-0.0," + "1.0," + "1.7976931348623157E308," + "4.9E-324," + "0.0," + "-0.5," + "2.2250738585072014E-308," + "3.141592653589793," + "2.718281828459045]"; JsonReader reader = newReader(json); reader.beginArray(); assertThat(reader.nextDouble()).isEqualTo(-0.0); assertThat(reader.nextDouble()).isEqualTo(1.0); assertThat(reader.nextDouble()).isEqualTo(1.7976931348623157E308); assertThat(reader.nextDouble()).isEqualTo(4.9E-324); assertThat(reader.nextDouble()).isEqualTo(0.0); assertThat(reader.nextDouble()).isEqualTo(-0.5); assertThat(reader.nextDouble()).isEqualTo(2.2250738585072014E-308); assertThat(reader.nextDouble()).isEqualTo(3.141592653589793); assertThat(reader.nextDouble()).isEqualTo(2.718281828459045); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void strictNonFiniteDoubles() throws IOException { String json = "[NaN]"; JsonReader reader = newReader(json); reader.beginArray(); try { reader.nextDouble(); fail(); } catch (JsonEncodingException expected) { } } @Test public void strictQuotedNonFiniteDoubles() throws IOException { String json = "[\"NaN\"]"; JsonReader reader = newReader(json); reader.beginArray(); try { reader.nextDouble(); fail(); } catch (JsonEncodingException expected) { assertThat(expected).hasMessageThat().contains("NaN"); } } @Test public void lenientNonFiniteDoubles() throws IOException { String json = "[NaN, -Infinity, Infinity]"; JsonReader reader = newReader(json); reader.setLenient(true); reader.beginArray(); assertThat(Double.isNaN(reader.nextDouble())).isTrue(); assertThat(reader.nextDouble()).isEqualTo(Double.NEGATIVE_INFINITY); assertThat(reader.nextDouble()).isEqualTo(Double.POSITIVE_INFINITY); reader.endArray(); } @Test public void lenientQuotedNonFiniteDoubles() throws IOException { String json = "[\"NaN\", \"-Infinity\", \"Infinity\"]"; JsonReader reader = newReader(json); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextDouble()).isNaN(); assertThat(reader.nextDouble()).isEqualTo(Double.NEGATIVE_INFINITY); assertThat(reader.nextDouble()).isEqualTo(Double.POSITIVE_INFINITY); reader.endArray(); } @Test public void longs() throws IOException { assumeTrue(factory.implementsStrictPrecision()); String json = "[0,0,0," + "1,1,1," + "-1,-1,-1," + "-9223372036854775808," + "9223372036854775807]"; JsonReader reader = newReader(json); reader.beginArray(); assertThat(reader.nextLong()).isEqualTo(0L); assertThat(reader.nextInt()).isEqualTo(0); assertThat(reader.nextDouble()).isEqualTo(0.0d); assertThat(reader.nextLong()).isEqualTo(1L); assertThat(reader.nextInt()).isEqualTo(1); assertThat(reader.nextDouble()).isEqualTo(1.0d); assertThat(reader.nextLong()).isEqualTo(-1L); assertThat(reader.nextInt()).isEqualTo(-1); assertThat(reader.nextDouble()).isEqualTo(-1.0d); try { reader.nextInt(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextLong()).isEqualTo(Long.MIN_VALUE); try { reader.nextInt(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextLong()).isEqualTo(Long.MAX_VALUE); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void booleans() throws IOException { JsonReader reader = newReader("[true,false]"); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); assertThat(reader.nextBoolean()).isFalse(); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void nextFailuresDoNotAdvance() throws IOException { JsonReader reader = newReader("{\"a\":true}"); reader.beginObject(); try { reader.nextString(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextName()).isEqualTo("a"); try { reader.nextName(); fail(); } catch (JsonDataException expected) { } try { reader.beginArray(); fail(); } catch (JsonDataException expected) { } try { reader.endArray(); fail(); } catch (JsonDataException expected) { } try { reader.beginObject(); fail(); } catch (JsonDataException expected) { } try { reader.endObject(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextBoolean()).isTrue(); try { reader.nextString(); fail(); } catch (JsonDataException expected) { } try { reader.nextName(); fail(); } catch (JsonDataException expected) { } try { reader.beginArray(); fail(); } catch (JsonDataException expected) { } try { reader.endArray(); fail(); } catch (JsonDataException expected) { } reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); reader.close(); } @Test public void integerMismatchWithDoubleDoesNotAdvance() throws IOException { assumeTrue(factory.implementsStrictPrecision()); JsonReader reader = newReader("[1.5]"); reader.beginArray(); try { reader.nextInt(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextDouble()).isEqualTo(1.5d); reader.endArray(); } @Test public void integerMismatchWithLongDoesNotAdvance() throws IOException { assumeTrue(factory.implementsStrictPrecision()); JsonReader reader = newReader("[9223372036854775807]"); reader.beginArray(); try { reader.nextInt(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextLong()).isEqualTo(9223372036854775807L); reader.endArray(); } @Test public void longMismatchWithDoubleDoesNotAdvance() throws IOException { assumeTrue(factory.implementsStrictPrecision()); JsonReader reader = newReader("[1.5]"); reader.beginArray(); try { reader.nextLong(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextDouble()).isEqualTo(1.5d); reader.endArray(); } @Test public void stringNullIsNotNull() throws IOException { JsonReader reader = newReader("[\"null\"]"); reader.beginArray(); try { reader.nextNull(); fail(); } catch (JsonDataException expected) { } } @Test public void nullLiteralIsNotAString() throws IOException { JsonReader reader = newReader("[null]"); reader.beginArray(); try { reader.nextString(); fail(); } catch (JsonDataException expected) { } } @Test public void topLevelValueTypes() throws IOException { JsonReader reader1 = newReader("true"); assertThat(reader1.nextBoolean()).isTrue(); assertThat(reader1.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); JsonReader reader2 = newReader("false"); assertThat(reader2.nextBoolean()).isFalse(); assertThat(reader2.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); JsonReader reader3 = newReader("null"); assertThat(reader3.nextNull()).isNull(); assertThat(reader3.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); JsonReader reader4 = newReader("123"); assertThat(reader4.nextInt()).isEqualTo(123); assertThat(reader4.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); JsonReader reader5 = newReader("123.4"); assertThat(reader5.nextDouble()).isEqualTo(123.4); assertThat(reader5.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); JsonReader reader6 = newReader("\"a\""); assertThat(reader6.nextString()).isEqualTo("a"); assertThat(reader6.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void topLevelValueTypeWithSkipValue() throws IOException { JsonReader reader = newReader("true"); reader.skipValue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void deeplyNestedArrays() throws IOException { JsonReader reader = newReader("[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"); for (int i = 0; i < 31; i++) { reader.beginArray(); } assertThat(reader.getPath()) .isEqualTo( "$[0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0][0]" + "[0][0][0][0][0][0][0][0][0][0][0][0][0]"); for (int i = 0; i < 31; i++) { reader.endArray(); } assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void deeplyNestedObjects() throws IOException { // Build a JSON document structured like {"a":{"a":{"a":{"a":true}}}}, but 31 levels deep. String array = "{\"a\":%s}"; String json = "true"; for (int i = 0; i < 31; i++) { json = String.format(array, json); } JsonReader reader = newReader(json); for (int i = 0; i < 31; i++) { reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); } assertThat(reader.getPath()) .isEqualTo("$.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a.a"); assertThat(reader.nextBoolean()).isTrue(); for (int i = 0; i < 31; i++) { reader.endObject(); } assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipVeryLongUnquotedString() throws IOException { JsonReader reader = newReader("[" + repeat('x', 8192) + "]"); reader.setLenient(true); reader.beginArray(); reader.skipValue(); reader.endArray(); } @Test public void skipTopLevelUnquotedString() throws IOException { JsonReader reader = newReader(repeat('x', 8192)); reader.setLenient(true); reader.skipValue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipVeryLongQuotedString() throws IOException { JsonReader reader = newReader("[\"" + repeat('x', 8192) + "\"]"); reader.beginArray(); reader.skipValue(); reader.endArray(); } @Test public void skipTopLevelQuotedString() throws IOException { JsonReader reader = newReader("\"" + repeat('x', 8192) + "\""); reader.setLenient(true); reader.skipValue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void stringAsNumberWithTruncatedExponent() throws IOException { JsonReader reader = newReader("[123e]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(STRING); } @Test public void stringAsNumberWithDigitAndNonDigitExponent() throws IOException { JsonReader reader = newReader("[123e4b]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(STRING); } @Test public void stringAsNumberWithNonDigitExponent() throws IOException { JsonReader reader = newReader("[123eb]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(STRING); } @Test public void emptyStringName() throws IOException { JsonReader reader = newReader("{\"\":true}"); reader.setLenient(true); assertThat(reader.peek()).isEqualTo(BEGIN_OBJECT); reader.beginObject(); assertThat(reader.peek()).isEqualTo(NAME); assertThat(reader.nextName()).isEqualTo(""); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BOOLEAN); assertThat(reader.nextBoolean()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void validEscapes() throws IOException { JsonReader reader = newReader("[\"\\\"\\\\\\/\\b\\f\\n\\r\\t\"]"); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("\"\\/\b\f\n\r\t"); } @Test public void selectName() throws IOException { JsonReader.Options abc = JsonReader.Options.of("a", "b", "c"); JsonReader reader = newReader("{\"a\": 5, \"b\": 5, \"c\": 5, \"d\": 5}"); reader.beginObject(); assertEquals("$.", reader.getPath()); assertEquals(0, reader.selectName(abc)); assertEquals("$.a", reader.getPath()); assertEquals(5, reader.nextInt()); assertEquals("$.a", reader.getPath()); assertEquals(1, reader.selectName(abc)); assertEquals("$.b", reader.getPath()); assertEquals(5, reader.nextInt()); assertEquals("$.b", reader.getPath()); assertEquals(2, reader.selectName(abc)); assertEquals("$.c", reader.getPath()); assertEquals(5, reader.nextInt()); assertEquals("$.c", reader.getPath()); // A missed selectName() doesn't advance anything, not even the path. assertEquals(-1, reader.selectName(abc)); assertEquals("$.c", reader.getPath()); assertEquals(JsonReader.Token.NAME, reader.peek()); assertEquals("d", reader.nextName()); assertEquals("$.d", reader.getPath()); assertEquals(5, reader.nextInt()); assertEquals("$.d", reader.getPath()); reader.endObject(); } /** Select does match necessarily escaping. The decoded value is used in the path. */ @Test public void selectNameNecessaryEscaping() throws IOException { JsonReader.Options options = JsonReader.Options.of("\n", "\u0000", "\""); JsonReader reader = newReader("{\"\\n\": 5,\"\\u0000\": 5, \"\\\"\": 5}"); reader.beginObject(); assertEquals(0, reader.selectName(options)); assertEquals(5, reader.nextInt()); assertEquals("$.\n", reader.getPath()); assertEquals(1, reader.selectName(options)); assertEquals(5, reader.nextInt()); assertEquals("$.\u0000", reader.getPath()); assertEquals(2, reader.selectName(options)); assertEquals(5, reader.nextInt()); assertEquals("$.\"", reader.getPath()); reader.endObject(); } /** Select removes unnecessary escaping from the source JSON. */ @Test public void selectNameUnnecessaryEscaping() throws IOException { JsonReader.Options options = JsonReader.Options.of("coffee", "tea"); JsonReader reader = newReader("{\"cof\\u0066ee\":5, \"\\u0074e\\u0061\":4, \"water\":3}"); reader.beginObject(); assertEquals(0, reader.selectName(options)); assertEquals(5, reader.nextInt()); assertEquals("$.coffee", reader.getPath()); assertEquals(1, reader.selectName(options)); assertEquals(4, reader.nextInt()); assertEquals("$.tea", reader.getPath()); // Ensure select name doesn't advance the stack in case there are no matches. assertEquals(-1, reader.selectName(options)); assertEquals(JsonReader.Token.NAME, reader.peek()); assertEquals("$.tea", reader.getPath()); // Consume the last token. assertEquals("water", reader.nextName()); assertEquals(3, reader.nextInt()); reader.endObject(); } @Test public void selectNameUnquoted() throws Exception { JsonReader.Options options = JsonReader.Options.of("a", "b"); JsonReader reader = newReader("{a:2}"); reader.setLenient(true); reader.beginObject(); assertEquals(0, reader.selectName(options)); assertEquals("$.a", reader.getPath()); assertEquals(2, reader.nextInt()); assertEquals("$.a", reader.getPath()); reader.endObject(); } @Test public void selectNameSingleQuoted() throws IOException { JsonReader.Options abc = JsonReader.Options.of("a", "b"); JsonReader reader = newReader("{'a':5}"); reader.setLenient(true); reader.beginObject(); assertEquals(0, reader.selectName(abc)); assertEquals("$.a", reader.getPath()); assertEquals(5, reader.nextInt()); assertEquals("$.a", reader.getPath()); reader.endObject(); } @Test public void selectString() throws IOException { JsonReader.Options abc = JsonReader.Options.of("a", "b", "c"); JsonReader reader = newReader("[\"a\", \"b\", \"c\", \"d\"]"); reader.beginArray(); assertEquals("$[0]", reader.getPath()); assertEquals(0, reader.selectString(abc)); assertEquals("$[1]", reader.getPath()); assertEquals(1, reader.selectString(abc)); assertEquals("$[2]", reader.getPath()); assertEquals(2, reader.selectString(abc)); assertEquals("$[3]", reader.getPath()); // A missed selectName() doesn't advance anything, not even the path. assertEquals(-1, reader.selectString(abc)); assertEquals("$[3]", reader.getPath()); assertEquals(JsonReader.Token.STRING, reader.peek()); assertEquals("d", reader.nextString()); assertEquals("$[4]", reader.getPath()); reader.endArray(); } @Test public void selectStringNecessaryEscaping() throws Exception { JsonReader.Options options = JsonReader.Options.of("\n", "\u0000", "\""); JsonReader reader = newReader("[\"\\n\",\"\\u0000\", \"\\\"\"]"); reader.beginArray(); assertEquals(0, reader.selectString(options)); assertEquals(1, reader.selectString(options)); assertEquals(2, reader.selectString(options)); reader.endArray(); } /** Select strips unnecessarily-escaped strings. */ @Test public void selectStringUnnecessaryEscaping() throws IOException { JsonReader.Options abc = JsonReader.Options.of("a", "b", "c"); JsonReader reader = newReader("[\"\\u0061\", \"b\", \"\\u0063\"]"); reader.beginArray(); assertEquals(0, reader.selectString(abc)); assertEquals(1, reader.selectString(abc)); assertEquals(2, reader.selectString(abc)); reader.endArray(); } @Test public void selectStringUnquoted() throws IOException { JsonReader.Options abc = JsonReader.Options.of("a", "b", "c"); JsonReader reader = newReader("[a, \"b\", c]"); reader.setLenient(true); reader.beginArray(); assertEquals(0, reader.selectString(abc)); assertEquals(1, reader.selectString(abc)); assertEquals(2, reader.selectString(abc)); reader.endArray(); } @Test public void selectStringSingleQuoted() throws IOException { JsonReader.Options abc = JsonReader.Options.of("a", "b", "c"); JsonReader reader = newReader("['a', \"b\", c]"); reader.setLenient(true); reader.beginArray(); assertEquals(0, reader.selectString(abc)); assertEquals(1, reader.selectString(abc)); assertEquals(2, reader.selectString(abc)); reader.endArray(); } @Test public void selectStringMaintainsReaderState() throws IOException { JsonReader.Options abc = JsonReader.Options.of("a", "b", "c"); JsonReader reader = newReader("[\"\\u0061\", \"42\"]"); reader.beginArray(); assertEquals(0, reader.selectString(abc)); assertEquals(-1, reader.selectString(abc)); assertEquals(JsonReader.Token.STRING, reader.peek()); // Next long can retrieve a value from a buffered string. assertEquals(42, reader.nextLong()); reader.endArray(); } @Test public void selectStringWithoutString() throws IOException { JsonReader.Options numbers = JsonReader.Options.of("1", "2.0", "true", "4"); JsonReader reader = newReader("[0, 2.0, true, \"4\"]"); reader.beginArray(); assertThat(reader.selectString(numbers)).isEqualTo(-1); reader.skipValue(); assertThat(reader.selectString(numbers)).isEqualTo(-1); reader.skipValue(); assertThat(reader.selectString(numbers)).isEqualTo(-1); reader.skipValue(); assertThat(reader.selectString(numbers)).isEqualTo(3); reader.endArray(); } @Test public void stringToNumberCoersion() throws Exception { JsonReader reader = newReader("[\"0\", \"9223372036854775807\", \"1.5\"]"); reader.beginArray(); assertThat(reader.nextInt()).isEqualTo(0); assertThat(reader.nextLong()).isEqualTo(9223372036854775807L); assertThat(reader.nextDouble()).isEqualTo(1.5d); reader.endArray(); } @Test public void unnecessaryPrecisionNumberCoersion() throws Exception { JsonReader reader = newReader("[\"0.0\", \"9223372036854775807.0\"]"); reader.beginArray(); assertThat(reader.nextInt()).isEqualTo(0); assertThat(reader.nextLong()).isEqualTo(9223372036854775807L); reader.endArray(); } @Test public void nanInfinityDoubleCoersion() throws Exception { JsonReader reader = newReader("[\"NaN\", \"Infinity\", \"-Infinity\"]"); reader.beginArray(); reader.setLenient(true); assertThat(reader.nextDouble()).isNaN(); assertThat(reader.nextDouble()).isEqualTo(Double.POSITIVE_INFINITY); assertThat(reader.nextDouble()).isEqualTo(Double.NEGATIVE_INFINITY); reader.endArray(); } @Test public void intMismatchWithStringDoesNotAdvance() throws Exception { JsonReader reader = newReader("[\"a\"]"); reader.beginArray(); try { reader.nextInt(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextString()).isEqualTo("a"); reader.endArray(); } @Test public void longMismatchWithStringDoesNotAdvance() throws Exception { JsonReader reader = newReader("[\"a\"]"); reader.beginArray(); try { reader.nextLong(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextString()).isEqualTo("a"); reader.endArray(); } @Test public void doubleMismatchWithStringDoesNotAdvance() throws Exception { JsonReader reader = newReader("[\"a\"]"); reader.beginArray(); try { reader.nextDouble(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextString()).isEqualTo("a"); reader.endArray(); } @Test public void readJsonValueInt() throws IOException { JsonReader reader = newReader("1"); Object value = reader.readJsonValue(); assertThat(value).isEqualTo(1.0); } @Test public void readJsonValueMap() throws IOException { JsonReader reader = newReader("{\"hello\": \"world\"}"); Object value = reader.readJsonValue(); assertThat(value).isEqualTo(Collections.singletonMap("hello", "world")); } @Test public void readJsonValueList() throws IOException { JsonReader reader = newReader("[\"a\", \"b\"]"); Object value = reader.readJsonValue(); assertThat(value).isEqualTo(Arrays.asList("a", "b")); } @Test public void readJsonValueListMultipleTypes() throws IOException { JsonReader reader = newReader("[\"a\", 5, false]"); Object value = reader.readJsonValue(); assertThat(value).isEqualTo(Arrays.asList("a", 5.0, false)); } @Test public void readJsonValueNestedListInMap() throws IOException { JsonReader reader = newReader("{\"pizzas\": [\"cheese\", \"pepperoni\"]}"); Object value = reader.readJsonValue(); assertThat(value) .isEqualTo(Collections.singletonMap("pizzas", Arrays.asList("cheese", "pepperoni"))); } @Test public void skipName() throws IOException { JsonReader reader = newReader("{\"a\":1}"); reader.beginObject(); reader.skipName(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NUMBER); reader.skipValue(); reader.endObject(); } @Test public void skipNameFailUnknown() throws IOException { JsonReader reader = newReader("{\"a\":1,\"b\":2}"); reader.setFailOnUnknown(true); reader.beginObject(); assertEquals("a", reader.nextName()); assertEquals(1, reader.nextInt()); try { reader.skipName(); fail(); } catch (JsonDataException e) { assertThat(e).hasMessageThat().isEqualTo("Cannot skip unexpected NAME at $.b"); } } @Test public void skipNameOnValueFails() throws IOException { JsonReader reader = newReader("1"); try { reader.skipName(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextInt()).isEqualTo(1); } @Test public void emptyDocumentHasNextReturnsFalse() throws IOException { JsonReader reader = newReader("1"); reader.readJsonValue(); assertThat(reader.hasNext()).isFalse(); } @Test public void skipValueAtEndOfObjectFails() throws IOException { JsonReader reader = newReader("{}"); reader.beginObject(); try { reader.skipValue(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected a value but was END_OBJECT at path $."); } reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipValueAtEndOfArrayFails() throws IOException { JsonReader reader = newReader("[]"); reader.beginArray(); try { reader.skipValue(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected a value but was END_ARRAY at path $[0]"); } reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipValueAtEndOfDocumentFails() throws IOException { JsonReader reader = newReader("1"); reader.nextInt(); try { reader.skipValue(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected a value but was END_DOCUMENT at path $"); } assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void basicPeekJson() throws IOException { JsonReader reader = newReader("{\"a\":12,\"b\":[34,56],\"c\":78}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextInt()).isEqualTo(12); assertThat(reader.nextName()).isEqualTo("b"); reader.beginArray(); assertThat(reader.nextInt()).isEqualTo(34); // Peek. JsonReader peekReader = reader.peekJson(); assertThat(peekReader.nextInt()).isEqualTo(56); peekReader.endArray(); assertThat(peekReader.nextName()).isEqualTo("c"); assertThat(peekReader.nextInt()).isEqualTo(78); peekReader.endObject(); assertThat(peekReader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); // Read again. assertThat(reader.nextInt()).isEqualTo(56); reader.endArray(); assertThat(reader.nextName()).isEqualTo("c"); assertThat(reader.nextInt()).isEqualTo(78); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } /** * We have a document that requires 12 operations to read. We read it step-by-step with one real * reader. Before each of the real reader’s operations we create a peeking reader and let it read * the rest of the document. */ @Test public void peekJsonReader() throws IOException { JsonReader reader = newReader("[12,34,{\"a\":56,\"b\":78},90]"); for (int i = 0; i < 12; i++) { readPeek12Steps(reader.peekJson(), i, 12); readPeek12Steps(reader, i, i + 1); } } /** * Read a fragment of {@code reader}. This assumes the fixed document defined in {@link * #peekJsonReader} and reads a range of it on each call. */ private void readPeek12Steps(JsonReader reader, int from, int until) throws IOException { switch (from) { case 0: if (until == 0) break; reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$[0]"); case 1: if (until == 1) break; assertThat(reader.nextInt()).isEqualTo(12); assertThat(reader.getPath()).isEqualTo("$[1]"); case 2: if (until == 2) break; assertThat(reader.nextInt()).isEqualTo(34); assertThat(reader.getPath()).isEqualTo("$[2]"); case 3: if (until == 3) break; reader.beginObject(); assertThat(reader.getPath()).isEqualTo("$[2]."); case 4: if (until == 4) break; assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.getPath()).isEqualTo("$[2].a"); case 5: if (until == 5) break; assertThat(reader.nextInt()).isEqualTo(56); assertThat(reader.getPath()).isEqualTo("$[2].a"); case 6: if (until == 6) break; assertThat(reader.nextName()).isEqualTo("b"); assertThat(reader.getPath()).isEqualTo("$[2].b"); case 7: if (until == 7) break; assertThat(reader.nextInt()).isEqualTo(78); assertThat(reader.getPath()).isEqualTo("$[2].b"); case 8: if (until == 8) break; reader.endObject(); assertThat(reader.getPath()).isEqualTo("$[3]"); case 9: if (until == 9) break; assertThat(reader.nextInt()).isEqualTo(90); assertThat(reader.getPath()).isEqualTo("$[4]"); case 10: if (until == 10) break; reader.endArray(); assertThat(reader.getPath()).isEqualTo("$"); case 11: if (until == 11) break; assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); assertThat(reader.getPath()).isEqualTo("$"); } } /** Confirm that we can peek in every state of the UTF-8 reader. */ @Test public void peekAfterPeek() throws IOException { JsonReader reader = newReader("[{\"a\":\"aaa\",'b':'bbb',c:c,\"d\":\"d\"},true,false,null,1,2.0]"); reader.setLenient(true); readValue(reader, true); reader.peekJson(); } @Test public void peekAfterPromoteNameToValue() throws IOException { JsonReader reader = newReader("{\"a\":\"b\"}"); reader.beginObject(); reader.promoteNameToValue(); assertEquals("a", reader.peekJson().nextString()); assertEquals("a", reader.nextString()); assertEquals("b", reader.peekJson().nextString()); assertEquals("b", reader.nextString()); reader.endObject(); } @Test public void promoteStringNameToValue() throws IOException { JsonReader reader = newReader("{\"a\":\"b\"}"); reader.beginObject(); reader.promoteNameToValue(); assertEquals("a", reader.nextString()); assertEquals("b", reader.nextString()); reader.endObject(); } @Test public void promoteDoubleNameToValue() throws IOException { JsonReader reader = newReader("{\"5\":\"b\"}"); reader.beginObject(); reader.promoteNameToValue(); assertEquals(5.0, reader.nextDouble(), 0.0); assertEquals("b", reader.nextString()); reader.endObject(); } @Test public void promoteLongNameToValue() throws IOException { JsonReader reader = newReader("{\"5\":\"b\"}"); reader.beginObject(); reader.promoteNameToValue(); assertEquals(5L, reader.nextLong()); assertEquals("b", reader.nextString()); reader.endObject(); } @Test public void promoteNullNameToValue() throws IOException { JsonReader reader = newReader("{\"null\":\"b\"}"); reader.beginObject(); reader.promoteNameToValue(); try { reader.nextNull(); fail(); } catch (JsonDataException expected) { } assertEquals("null", reader.nextString()); } @Test public void promoteBooleanNameToValue() throws IOException { JsonReader reader = newReader("{\"true\":\"b\"}"); reader.beginObject(); reader.promoteNameToValue(); try { reader.nextBoolean(); fail(); } catch (JsonDataException expected) { } assertEquals("true", reader.nextString()); } @Test public void promoteBooleanNameToValueCannotBeReadAsName() throws IOException { JsonReader reader = newReader("{\"true\":\"b\"}"); reader.beginObject(); reader.promoteNameToValue(); try { reader.nextName(); fail(); } catch (JsonDataException expected) { } assertEquals("true", reader.nextString()); } @Test public void promoteSkippedNameToValue() throws IOException { JsonReader reader = newReader("{\"true\":\"b\"}"); reader.beginObject(); reader.promoteNameToValue(); reader.skipValue(); assertEquals("b", reader.nextString()); } @Test public void promoteNameToValueAtEndOfObject() throws IOException { JsonReader reader = newReader("{}"); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.hasNext()).isFalse(); reader.endObject(); } @Test public void optionsStrings() { String[] options = new String[] {"a", "b", "c"}; JsonReader.Options abc = JsonReader.Options.of("a", "b", "c"); List strings = abc.strings(); assertThat(options).asList().containsExactlyElementsIn(strings).inOrder(); try { // Confirm it's unmodifiable and we can't mutate the original underlying array strings.add("d"); fail(); } catch (UnsupportedOperationException expected) { } } @Test public void nextSourceString() throws IOException { // language=JSON JsonReader reader = newReader("{\"a\":\"this is a string\"}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("\"this is a string\""); } } @Test public void nextSourceLong() throws IOException { // language=JSON JsonReader reader = newReader("{\"a\":-2.0}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("-2.0"); } } @Test public void nextSourceNull() throws IOException { // language=JSON JsonReader reader = newReader("{\"a\":null}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("null"); } } @Test public void nextSourceBoolean() throws IOException { // language=JSON JsonReader reader = newReader("{\"a\":false}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("false"); } } @Test public void nextSourceObject() throws IOException { // language=JSON JsonReader reader = newReader("{\"a\":{\"b\":2.0,\"c\":3.0}}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("{\"b\":2.0,\"c\":3.0}"); } } @Test public void nextSourceArray() throws IOException { // language=JSON JsonReader reader = newReader("{\"a\":[2.0,2.0,3.0]}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("[2.0,2.0,3.0]"); } } /** * When we call {@link JsonReader#selectString} it causes the reader to consume bytes of the input * string. When attempting to read it as a stream afterwards the bytes are reconstructed. */ @Test public void nextSourceStringBuffered() throws IOException { // language=JSON JsonReader reader = newReader("{\"a\":\"b\"}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.selectString(JsonReader.Options.of("x'"))).isEqualTo(-1); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("\"b\""); } } /** If we don't read the bytes of the source, they JsonReader doesn't lose its place. */ @Test public void nextSourceNotConsumed() throws IOException { // language=JSON JsonReader reader = newReader("{\"a\":\"b\",\"c\":\"d\"}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); reader.nextSource(); // Not closed. assertThat(reader.nextName()).isEqualTo("c"); assertThat(reader.nextString()).isEqualTo("d"); } @SuppressWarnings("rawtypes") @Test public void tags() throws IOException { JsonReader reader = newReader("{}"); assertThat(reader.tag(Integer.class)).isNull(); assertThat(reader.tag(CharSequence.class)).isNull(); reader.setTag(Integer.class, 1); reader.setTag(CharSequence.class, "Foo"); try { reader.setTag((Class) CharSequence.class, 1); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Tag value must be of type java.lang.CharSequence"); } Object intTag = reader.tag(Integer.class); assertThat(intTag).isEqualTo(1); assertThat(intTag).isInstanceOf(Integer.class); Object charSequenceTag = reader.tag(CharSequence.class); assertThat(charSequenceTag).isEqualTo("Foo"); assertThat(charSequenceTag).isInstanceOf(String.class); assertThat(reader.tag(String.class)).isNull(); } /** Peek a value, then read it, recursively. */ private void readValue(JsonReader reader, boolean peekJsonFirst) throws IOException { JsonReader.Token token = reader.peek(); if (peekJsonFirst) { readValue(reader.peekJson(), false); } switch (token) { case BEGIN_ARRAY: reader.beginArray(); while (reader.hasNext()) { readValue(reader, peekJsonFirst); } reader.peekJson().endArray(); reader.endArray(); break; case BEGIN_OBJECT: reader.beginObject(); while (reader.hasNext()) { assertNotNull(reader.peekJson().nextName()); assertNotNull(reader.nextName()); readValue(reader, peekJsonFirst); } reader.peekJson().endObject(); reader.endObject(); break; case STRING: reader.nextString(); break; case NUMBER: reader.nextDouble(); break; case BOOLEAN: reader.nextBoolean(); break; case NULL: reader.nextNull(); break; default: throw new AssertionError(); } } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonUtf8ReaderTest.java ================================================ /* * Copyright (C) 2010 Google 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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static com.squareup.moshi.JsonReader.Token.BEGIN_ARRAY; import static com.squareup.moshi.JsonReader.Token.BEGIN_OBJECT; import static com.squareup.moshi.JsonReader.Token.BOOLEAN; import static com.squareup.moshi.JsonReader.Token.END_ARRAY; import static com.squareup.moshi.JsonReader.Token.END_OBJECT; import static com.squareup.moshi.JsonReader.Token.NAME; import static com.squareup.moshi.JsonReader.Token.NULL; import static com.squareup.moshi.JsonReader.Token.NUMBER; import static com.squareup.moshi.JsonReader.Token.STRING; import static com.squareup.moshi.TestUtil.MAX_DEPTH; import static com.squareup.moshi.TestUtil.newReader; import static com.squareup.moshi.TestUtil.repeat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import java.io.EOFException; import java.io.IOException; import java.util.Arrays; import okio.Buffer; import okio.BufferedSource; import okio.ForwardingSource; import okio.Okio; import okio.Source; import org.junit.Ignore; import org.junit.Test; public final class JsonUtf8ReaderTest { @Test public void readingDoesNotBuffer() throws IOException { Buffer buffer = new Buffer().writeUtf8("{}{}"); JsonReader reader1 = JsonReader.of(buffer); reader1.beginObject(); reader1.endObject(); assertThat(buffer.size()).isEqualTo(2); JsonReader reader2 = JsonReader.of(buffer); reader2.beginObject(); reader2.endObject(); assertThat(buffer.size()).isEqualTo(0); } @Test public void readObjectBuffer() throws IOException { Buffer buffer = new Buffer().writeUtf8("{\"a\": \"android\", \"b\": \"banana\"}"); JsonReader reader = JsonReader.of(buffer); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextString()).isEqualTo("android"); assertThat(reader.nextName()).isEqualTo("b"); assertThat(reader.nextString()).isEqualTo("banana"); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void readObjectSource() throws IOException { Buffer buffer = new Buffer().writeUtf8("{\"a\": \"android\", \"b\": \"banana\"}"); JsonReader reader = JsonReader.of(Okio.buffer(new ForwardingSource(buffer) {})); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextString()).isEqualTo("android"); assertThat(reader.nextName()).isEqualTo("b"); assertThat(reader.nextString()).isEqualTo("banana"); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void nullSource() { try { JsonReader.of(null); fail(); } catch (NullPointerException expected) { } } @Test public void unescapingInvalidCharacters() throws IOException { String json = "[\"\\u000g\"]"; JsonReader reader = newReader(json); reader.beginArray(); try { reader.nextString(); fail(); } catch (JsonEncodingException expected) { } } @Test public void unescapingTruncatedCharacters() throws IOException { String json = "[\"\\u000"; JsonReader reader = newReader(json); reader.beginArray(); try { reader.nextString(); fail(); } catch (EOFException expected) { } } @Test public void unescapingTruncatedSequence() throws IOException { String json = "[\"\\"; JsonReader reader = newReader(json); reader.beginArray(); try { reader.nextString(); fail(); } catch (JsonEncodingException expected) { } } @Test public void strictNonFiniteDoublesWithSkipValue() throws IOException { String json = "[NaN]"; JsonReader reader = newReader(json); reader.beginArray(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test @Ignore public void numberWithOctalPrefix() throws IOException { String json = "[01]"; JsonReader reader = newReader(json); reader.beginArray(); try { reader.peek(); fail(); } catch (JsonEncodingException expected) { } try { reader.nextInt(); fail(); } catch (JsonEncodingException expected) { } try { reader.nextLong(); fail(); } catch (JsonEncodingException expected) { } try { reader.nextDouble(); fail(); } catch (JsonEncodingException expected) { } assertThat(reader.nextString()).isEqualTo("01"); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void peekingUnquotedStringsPrefixedWithBooleans() throws IOException { JsonReader reader = newReader("[truey]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(STRING); try { reader.nextBoolean(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextString()).isEqualTo("truey"); reader.endArray(); } @Test public void malformedNumbers() throws IOException { assertNotANumber("-"); assertNotANumber("."); // exponent lacks digit assertNotANumber("e"); assertNotANumber("0e"); assertNotANumber(".e"); assertNotANumber("0.e"); assertNotANumber("-.0e"); // no integer assertNotANumber("e1"); assertNotANumber(".e1"); assertNotANumber("-e1"); // trailing characters assertNotANumber("1x"); assertNotANumber("1.1x"); assertNotANumber("1e1x"); assertNotANumber("1ex"); assertNotANumber("1.1ex"); assertNotANumber("1.1e1x"); // fraction has no digit assertNotANumber("0."); assertNotANumber("-0."); assertNotANumber("0.e1"); assertNotANumber("-0.e1"); // no leading digit assertNotANumber(".0"); assertNotANumber("-.0"); assertNotANumber(".0e1"); assertNotANumber("-.0e1"); } private void assertNotANumber(String s) throws IOException { JsonReader reader = newReader("[" + s + "]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextString()).isEqualTo(s); reader.endArray(); } @Test public void peekingUnquotedStringsPrefixedWithIntegers() throws IOException { JsonReader reader = newReader("[12.34e5x]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(STRING); try { reader.nextInt(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextString()).isEqualTo("12.34e5x"); } @Test public void peekLongMinValue() throws IOException { JsonReader reader = newReader("[-9223372036854775808]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(NUMBER); assertThat(reader.nextLong()).isEqualTo(-9223372036854775808L); } @Test public void peekLongMaxValue() throws IOException { JsonReader reader = newReader("[9223372036854775807]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(NUMBER); assertThat(reader.nextLong()).isEqualTo(9223372036854775807L); } @Test public void longLargerThanMaxLongThatWrapsAround() throws IOException { JsonReader reader = newReader("[22233720368547758070]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(NUMBER); try { reader.nextLong(); fail(); } catch (JsonDataException expected) { } } @Test public void longLargerThanMinLongThatWrapsAround() throws IOException { JsonReader reader = newReader("[-22233720368547758070]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(NUMBER); try { reader.nextLong(); fail(); } catch (JsonDataException expected) { } } @Test public void peekLargerThanLongMaxValue() throws IOException { JsonReader reader = newReader("[9223372036854775808]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(NUMBER); try { reader.nextLong(); fail(); } catch (JsonDataException expected) { } } @Test public void precisionNotDiscarded() throws IOException { JsonReader reader = newReader("[9223372036854775806.5]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(NUMBER); try { reader.nextLong(); fail(); } catch (JsonDataException expected) { } } @Test public void peekLargerThanLongMinValue() throws IOException { JsonReader reader = newReader("[-9223372036854775809]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(NUMBER); try { reader.nextLong(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextDouble()).isEqualTo(-9223372036854775809d); } @Test public void highPrecisionLong() throws IOException { String json = "[9223372036854775806.000]"; JsonReader reader = newReader(json); reader.beginArray(); assertThat(reader.nextLong()).isEqualTo(9223372036854775806L); reader.endArray(); } @Test public void peekMuchLargerThanLongMinValue() throws IOException { JsonReader reader = newReader("[-92233720368547758080]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(NUMBER); try { reader.nextLong(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextDouble()).isEqualTo(-92233720368547758080d); } @Test public void negativeZeroIsANumber() throws Exception { JsonReader reader = newReader("-0"); assertEquals(NUMBER, reader.peek()); assertEquals("-0", reader.nextString()); } @Test public void numberToStringCoersion() throws Exception { JsonReader reader = newReader("[0, 9223372036854775807, 2.5, 3.010, \"a\", \"5\"]"); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("0"); assertThat(reader.nextString()).isEqualTo("9223372036854775807"); assertThat(reader.nextString()).isEqualTo("2.5"); assertThat(reader.nextString()).isEqualTo("3.010"); assertThat(reader.nextString()).isEqualTo("a"); assertThat(reader.nextString()).isEqualTo("5"); reader.endArray(); } @Test public void quotedNumberWithEscape() throws IOException { JsonReader reader = newReader("[\"12\u00334\"]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(STRING); assertThat(reader.nextInt()).isEqualTo(1234); } @Test public void mixedCaseLiterals() throws IOException { JsonReader reader = newReader("[True,TruE,False,FALSE,NULL,nulL]"); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); assertThat(reader.nextBoolean()).isTrue(); assertThat(reader.nextBoolean()).isFalse(); assertThat(reader.nextBoolean()).isFalse(); reader.nextNull(); reader.nextNull(); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void missingValue() throws IOException { JsonReader reader = newReader("{\"a\":}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { reader.nextString(); fail(); } catch (JsonEncodingException expected) { } } @Test public void prematureEndOfInput() throws IOException { JsonReader reader = newReader("{\"a\":true,"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextBoolean()).isTrue(); try { reader.nextName(); fail(); } catch (EOFException expected) { } } @SuppressWarnings("CheckReturnValue") @Test public void prematurelyClosed() throws IOException { try { JsonReader reader = newReader("{\"a\":[]}"); reader.beginObject(); reader.close(); reader.nextName(); fail(); } catch (IllegalStateException expected) { } try { JsonReader reader = newReader("{\"a\":[]}"); reader.close(); reader.beginObject(); fail(); } catch (IllegalStateException expected) { } try { JsonReader reader = newReader("{\"a\":true}"); reader.beginObject(); reader.nextName(); reader.peek(); reader.close(); reader.nextBoolean(); fail(); } catch (IllegalStateException expected) { } } @Test public void strictNameValueSeparator() throws IOException { JsonReader reader = newReader("{\"a\"=true}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { reader.nextBoolean(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("{\"a\"=>true}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { reader.nextBoolean(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientNameValueSeparator() throws IOException { JsonReader reader = newReader("{\"a\"=true}"); reader.setLenient(true); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextBoolean()).isTrue(); reader = newReader("{\"a\"=>true}"); reader.setLenient(true); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextBoolean()).isTrue(); } @Test public void strictNameValueSeparatorWithSkipValue() throws IOException { JsonReader reader = newReader("{\"a\"=true}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("{\"a\"=>true}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test public void commentsInStringValue() throws Exception { JsonReader reader = newReader("[\"// comment\"]"); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("// comment"); reader.endArray(); reader = newReader("{\"a\":\"#someComment\"}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextString()).isEqualTo("#someComment"); reader.endObject(); reader = newReader("{\"#//a\":\"#some //Comment\"}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("#//a"); assertThat(reader.nextString()).isEqualTo("#some //Comment"); reader.endObject(); } @Test public void strictComments() throws IOException { JsonReader reader = newReader("[// comment \n true]"); reader.beginArray(); try { reader.nextBoolean(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("[# comment \n true]"); reader.beginArray(); try { reader.nextBoolean(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("[/* comment */ true]"); reader.beginArray(); try { reader.nextBoolean(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientComments() throws IOException { JsonReader reader = newReader("[// comment \n true]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); reader = newReader("[# comment \n true]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); reader = newReader("[/* comment */ true]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); reader = newReader("a//"); reader.setLenient(true); assertThat(reader.nextString()).isEqualTo("a"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void strictCommentsWithSkipValue() throws IOException { JsonReader reader = newReader("[// comment \n true]"); reader.beginArray(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("[# comment \n true]"); reader.beginArray(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("[/* comment */ true]"); reader.beginArray(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test public void strictUnquotedNames() throws IOException { JsonReader reader = newReader("{a:true}"); reader.beginObject(); try { reader.nextName(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientUnquotedNames() throws IOException { JsonReader reader = newReader("{a:true}"); reader.setLenient(true); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); } @Test public void jsonIsSingleUnquotedString() throws IOException { JsonReader reader = newReader("abc"); reader.setLenient(true); assertThat(reader.nextString()).isEqualTo("abc"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void strictUnquotedNamesWithSkipValue() throws IOException { JsonReader reader = newReader("{a:true}"); reader.beginObject(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test public void strictSingleQuotedNames() throws IOException { JsonReader reader = newReader("{'a':true}"); reader.beginObject(); try { reader.nextName(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientSingleQuotedNames() throws IOException { JsonReader reader = newReader("{'a':true}"); reader.setLenient(true); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); } @Test public void strictSingleQuotedNamesWithSkipValue() throws IOException { JsonReader reader = newReader("{'a':true}"); reader.beginObject(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test public void strictUnquotedStrings() throws IOException { JsonReader reader = newReader("[a]"); reader.beginArray(); try { reader.nextString(); fail(); } catch (JsonEncodingException expected) { } } @Test public void strictUnquotedStringsWithSkipValue() throws IOException { JsonReader reader = newReader("[a]"); reader.beginArray(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientUnquotedStrings() throws IOException { JsonReader reader = newReader("[a]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("a"); } @Test public void lenientUnquotedStringsDelimitedByComment() throws IOException { JsonReader reader = newReader("[a#comment\n]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("a"); reader.endArray(); } @Test public void strictSingleQuotedStrings() throws IOException { JsonReader reader = newReader("['a']"); reader.beginArray(); try { reader.nextString(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientSingleQuotedStrings() throws IOException { JsonReader reader = newReader("['a']"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("a"); } @Test public void strictSingleQuotedStringsWithSkipValue() throws IOException { JsonReader reader = newReader("['a']"); reader.beginArray(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test public void strictSemicolonDelimitedArray() throws IOException { JsonReader reader = newReader("[true;true]"); reader.beginArray(); try { reader.nextBoolean(); reader.nextBoolean(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientSemicolonDelimitedArray() throws IOException { JsonReader reader = newReader("[true;true]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); assertThat(reader.nextBoolean()).isTrue(); } @Test public void strictSemicolonDelimitedArrayWithSkipValue() throws IOException { JsonReader reader = newReader("[true;true]"); reader.beginArray(); try { reader.skipValue(); reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test public void strictSemicolonDelimitedNameValuePair() throws IOException { JsonReader reader = newReader("{\"a\":true;\"b\":true}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { reader.nextBoolean(); reader.nextName(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientSemicolonDelimitedNameValuePair() throws IOException { JsonReader reader = newReader("{\"a\":true;\"b\":true}"); reader.setLenient(true); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextBoolean()).isTrue(); assertThat(reader.nextName()).isEqualTo("b"); } @Test public void strictSemicolonDelimitedNameValuePairWithSkipValue() throws IOException { JsonReader reader = newReader("{\"a\":true;\"b\":true}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try { reader.skipValue(); reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test public void strictUnnecessaryArraySeparators() throws IOException { JsonReader reader = newReader("[true,,true]"); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); try { reader.nextNull(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("[,true]"); reader.beginArray(); try { reader.nextNull(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("[true,]"); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); try { reader.nextNull(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("[,]"); reader.beginArray(); try { reader.nextNull(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientUnnecessaryArraySeparators() throws IOException { JsonReader reader = newReader("[true,,true]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); reader.nextNull(); assertThat(reader.nextBoolean()).isTrue(); reader.endArray(); reader = newReader("[,true]"); reader.setLenient(true); reader.beginArray(); reader.nextNull(); assertThat(reader.nextBoolean()).isTrue(); reader.endArray(); reader = newReader("[true,]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); reader.nextNull(); reader.endArray(); reader = newReader("[,]"); reader.setLenient(true); reader.beginArray(); reader.nextNull(); reader.nextNull(); reader.endArray(); } @Test public void strictUnnecessaryArraySeparatorsWithSkipValue() throws IOException { JsonReader reader = newReader("[true,,true]"); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("[,true]"); reader.beginArray(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("[true,]"); reader.beginArray(); assertThat(reader.nextBoolean()).isTrue(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } reader = newReader("[,]"); reader.beginArray(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test public void strictMultipleTopLevelValues() throws IOException { JsonReader reader = newReader("[] []"); reader.beginArray(); reader.endArray(); try { reader.peek(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientMultipleTopLevelValues() throws IOException { JsonReader reader = newReader("[] true {}"); reader.setLenient(true); reader.beginArray(); reader.endArray(); assertThat(reader.nextBoolean()).isTrue(); reader.beginObject(); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void strictMultipleTopLevelValuesWithSkipValue() throws IOException { JsonReader reader = newReader("[] []"); reader.beginArray(); reader.endArray(); try { reader.skipValue(); fail(); } catch (JsonEncodingException expected) { } } @Test @Ignore public void bomIgnoredAsFirstCharacterOfDocument() throws IOException { JsonReader reader = newReader("\ufeff[]"); reader.beginArray(); reader.endArray(); } @Test public void bomForbiddenAsOtherCharacterInDocument() throws IOException { JsonReader reader = newReader("[\ufeff]"); reader.beginArray(); try { reader.endArray(); fail(); } catch (JsonEncodingException expected) { } } @Test public void failWithPosition() throws IOException { testFailWithPosition("Expected value at path $[1]", "[\n\n\n\n\n\"a\",}]"); } @Test public void failWithPositionGreaterThanBufferSize() throws IOException { String spaces = repeat(' ', 8192); testFailWithPosition("Expected value at path $[1]", "[\n\n" + spaces + "\n\n\n\"a\",}]"); } @Test public void failWithPositionOverSlashSlashEndOfLineComment() throws IOException { testFailWithPosition("Expected value at path $[1]", "\n// foo\n\n//bar\r\n[\"a\",}"); } @Test public void failWithPositionOverHashEndOfLineComment() throws IOException { testFailWithPosition("Expected value at path $[1]", "\n# foo\n\n#bar\r\n[\"a\",}"); } @Test public void failWithPositionOverCStyleComment() throws IOException { testFailWithPosition("Expected value at path $[1]", "\n\n/* foo\n*\n*\r\nbar */[\"a\",}"); } @Test public void failWithPositionOverQuotedString() throws IOException { testFailWithPosition("Expected value at path $[1]", "[\"foo\nbar\r\nbaz\n\",\n }"); } @Test public void failWithPositionOverUnquotedString() throws IOException { testFailWithPosition("Expected value at path $[1]", "[\n\nabcd\n\n,}"); } @Test public void failWithEscapedNewlineCharacter() throws IOException { testFailWithPosition("Expected value at path $[1]", "[\n\n\"\\\n\n\",}"); } @Test @Ignore public void failWithPositionIsOffsetByBom() throws IOException { testFailWithPosition("Expected value at path $[1]", "\ufeff[\"a\",}]"); } private void testFailWithPosition(String message, String json) throws IOException { // Validate that it works reading the string normally. JsonReader reader1 = newReader(json); reader1.setLenient(true); reader1.beginArray(); reader1.nextString(); try { reader1.peek(); fail(); } catch (JsonEncodingException expected) { assertThat(expected).hasMessageThat().isEqualTo(message); } // Also validate that it works when skipping. JsonReader reader2 = newReader(json); reader2.setLenient(true); reader2.beginArray(); reader2.skipValue(); try { reader2.peek(); fail(); } catch (JsonEncodingException expected) { assertThat(expected).hasMessageThat().isEqualTo(message); } } @SuppressWarnings("CheckReturnValue") @Test public void failWithPositionDeepPath() throws IOException { JsonReader reader = newReader("[1,{\"a\":[2,3,}"); reader.beginArray(); reader.nextInt(); reader.beginObject(); reader.nextName(); reader.beginArray(); reader.nextInt(); reader.nextInt(); try { reader.peek(); fail(); } catch (JsonEncodingException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected value at path $[1].a[2]"); } } @Test public void failureMessagePathFromSkipName() throws IOException { JsonReader reader = newReader("{\"a\":[42,}"); reader.beginObject(); reader.skipName(); reader.beginArray(); reader.nextInt(); try { reader.peek(); fail(); } catch (JsonEncodingException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected value at path $.null[1]"); } } @Test @Ignore public void strictVeryLongNumber() throws IOException { JsonReader reader = newReader("[0." + repeat('9', 8192) + "]"); reader.beginArray(); try { assertThat(reader.nextDouble()).isEqualTo(1d); fail(); } catch (JsonEncodingException expected) { } } @Test @Ignore public void lenientVeryLongNumber() throws IOException { JsonReader reader = newReader("[0." + repeat('9', 8192) + "]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextDouble()).isEqualTo(1d); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void veryLongUnquotedLiteral() throws IOException { String literal = "a" + repeat('b', 8192) + "c"; JsonReader reader = newReader("[" + literal + "]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextString()).isEqualTo(literal); reader.endArray(); } @Test public void tooDeeplyNestedArrays() throws IOException { JsonReader reader = newReader(repeat("[", MAX_DEPTH + 1) + repeat("]", MAX_DEPTH + 1)); for (int i = 0; i < MAX_DEPTH; i++) { reader.beginArray(); } try { reader.beginArray(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Nesting too deep at $" + repeat("[0]", MAX_DEPTH)); } } @Test public void tooDeeplyNestedObjects() throws IOException { // Build a JSON document structured like {"a":{"a":{"a":{"a":true}}}}, but 255 levels deep. String array = "{\"a\":%s}"; String json = "true"; for (int i = 0; i < MAX_DEPTH + 1; i++) { json = String.format(array, json); } JsonReader reader = newReader(json); for (int i = 0; i < MAX_DEPTH; i++) { reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); } try { reader.beginObject(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Nesting too deep at $" + repeat(".a", MAX_DEPTH)); } } // http://code.google.com/p/google-gson/issues/detail?id=409 @Test public void stringEndingInSlash() throws IOException { JsonReader reader = newReader("/"); reader.setLenient(true); try { reader.peek(); fail(); } catch (JsonEncodingException expected) { } } @Test public void documentWithCommentEndingInSlash() throws IOException { JsonReader reader = newReader("/* foo *//"); reader.setLenient(true); try { reader.peek(); fail(); } catch (JsonEncodingException expected) { } } @Test public void stringWithLeadingSlash() throws IOException { JsonReader reader = newReader("/x"); reader.setLenient(true); try { reader.peek(); fail(); } catch (JsonEncodingException expected) { } } @Test public void unterminatedObject() throws IOException { JsonReader reader = newReader("{\"a\":\"android\"x"); reader.setLenient(true); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextString()).isEqualTo("android"); try { reader.peek(); fail(); } catch (JsonEncodingException expected) { } } @Test public void veryLongQuotedString() throws IOException { char[] stringChars = new char[1024 * 16]; Arrays.fill(stringChars, 'x'); String string = new String(stringChars); String json = "[\"" + string + "\"]"; JsonReader reader = newReader(json); reader.beginArray(); assertThat(reader.nextString()).isEqualTo(string); reader.endArray(); } @Test public void veryLongUnquotedString() throws IOException { char[] stringChars = new char[1024 * 16]; Arrays.fill(stringChars, 'x'); String string = new String(stringChars); String json = "[" + string + "]"; JsonReader reader = newReader(json); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextString()).isEqualTo(string); reader.endArray(); } @Test public void veryLongUnterminatedString() throws IOException { char[] stringChars = new char[1024 * 16]; Arrays.fill(stringChars, 'x'); String string = new String(stringChars); String json = "[" + string; JsonReader reader = newReader(json); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextString()).isEqualTo(string); try { reader.peek(); fail(); } catch (EOFException expected) { } } @Test public void strictExtraCommasInMaps() throws IOException { JsonReader reader = newReader("{\"a\":\"b\",}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextString()).isEqualTo("b"); try { reader.peek(); fail(); } catch (JsonEncodingException expected) { } } @Test public void lenientExtraCommasInMaps() throws IOException { JsonReader reader = newReader("{\"a\":\"b\",}"); reader.setLenient(true); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextString()).isEqualTo("b"); try { reader.peek(); fail(); } catch (JsonEncodingException expected) { } } @Test public void malformedDocuments() throws IOException { assertDocument("{]", BEGIN_OBJECT, JsonEncodingException.class); assertDocument("{,", BEGIN_OBJECT, JsonEncodingException.class); assertDocument("{{", BEGIN_OBJECT, JsonEncodingException.class); assertDocument("{[", BEGIN_OBJECT, JsonEncodingException.class); assertDocument("{:", BEGIN_OBJECT, JsonEncodingException.class); assertDocument("{\"name\",", BEGIN_OBJECT, NAME, JsonEncodingException.class); assertDocument("{\"name\":}", BEGIN_OBJECT, NAME, JsonEncodingException.class); assertDocument("{\"name\"::", BEGIN_OBJECT, NAME, JsonEncodingException.class); assertDocument("{\"name\":,", BEGIN_OBJECT, NAME, JsonEncodingException.class); assertDocument("{\"name\"=}", BEGIN_OBJECT, NAME, JsonEncodingException.class); assertDocument("{\"name\"=>}", BEGIN_OBJECT, NAME, JsonEncodingException.class); assertDocument( "{\"name\"=>\"string\":", BEGIN_OBJECT, NAME, STRING, JsonEncodingException.class); assertDocument( "{\"name\"=>\"string\"=", BEGIN_OBJECT, NAME, STRING, JsonEncodingException.class); assertDocument( "{\"name\"=>\"string\"=>", BEGIN_OBJECT, NAME, STRING, JsonEncodingException.class); assertDocument("{\"name\"=>\"string\",", BEGIN_OBJECT, NAME, STRING, EOFException.class); assertDocument("{\"name\"=>\"string\",\"name\"", BEGIN_OBJECT, NAME, STRING, NAME); assertDocument("[}", BEGIN_ARRAY, JsonEncodingException.class); assertDocument("[,]", BEGIN_ARRAY, NULL, NULL, END_ARRAY); assertDocument("{", BEGIN_OBJECT, EOFException.class); assertDocument("{\"name\"", BEGIN_OBJECT, NAME, EOFException.class); assertDocument("{\"name\",", BEGIN_OBJECT, NAME, JsonEncodingException.class); assertDocument("{'name'", BEGIN_OBJECT, NAME, EOFException.class); assertDocument("{'name',", BEGIN_OBJECT, NAME, JsonEncodingException.class); assertDocument("{name", BEGIN_OBJECT, NAME, EOFException.class); assertDocument("[", BEGIN_ARRAY, EOFException.class); assertDocument("[string", BEGIN_ARRAY, STRING, EOFException.class); assertDocument("[\"string\"", BEGIN_ARRAY, STRING, EOFException.class); assertDocument("['string'", BEGIN_ARRAY, STRING, EOFException.class); assertDocument("[123", BEGIN_ARRAY, NUMBER, EOFException.class); assertDocument("[123,", BEGIN_ARRAY, NUMBER, EOFException.class); assertDocument("{\"name\":123", BEGIN_OBJECT, NAME, NUMBER, EOFException.class); assertDocument("{\"name\":123,", BEGIN_OBJECT, NAME, NUMBER, EOFException.class); assertDocument("{\"name\":\"string\"", BEGIN_OBJECT, NAME, STRING, EOFException.class); assertDocument("{\"name\":\"string\",", BEGIN_OBJECT, NAME, STRING, EOFException.class); assertDocument("{\"name\":'string'", BEGIN_OBJECT, NAME, STRING, EOFException.class); assertDocument("{\"name\":'string',", BEGIN_OBJECT, NAME, STRING, EOFException.class); assertDocument("{\"name\":false", BEGIN_OBJECT, NAME, BOOLEAN, EOFException.class); assertDocument("{\"name\":false,,", BEGIN_OBJECT, NAME, BOOLEAN, JsonEncodingException.class); } /** * This test behave slightly differently in Gson 2.2 and earlier. It fails during peek rather than * during nextString(). */ @Test public void unterminatedStringFailure() throws IOException { JsonReader reader = newReader("[\"string"); reader.setLenient(true); reader.beginArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); try { reader.nextString(); fail(); } catch (JsonEncodingException expected) { } } @Test public void invalidEscape() throws IOException { JsonReader reader = newReader("[\"str\\ing\"]"); reader.beginArray(); try { reader.nextString(); fail(); } catch (JsonEncodingException expected) { assertThat(expected).hasMessageThat().isEqualTo("Invalid escape sequence: \\i at path $[0]"); } } @Test public void lenientInvalidEscape() throws IOException { JsonReader reader = newReader("[\"str\\ing\"]"); reader.setLenient(true); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("string"); } private void assertDocument(String document, Object... expectations) throws IOException { JsonReader reader = newReader(document); reader.setLenient(true); for (Object expectation : expectations) { if (expectation == BEGIN_OBJECT) { reader.beginObject(); } else if (expectation == BEGIN_ARRAY) { reader.beginArray(); } else if (expectation == END_OBJECT) { reader.endObject(); } else if (expectation == END_ARRAY) { reader.endArray(); } else if (expectation == NAME) { assertThat(reader.nextName()).isEqualTo("name"); } else if (expectation == BOOLEAN) { assertThat(reader.nextBoolean()).isFalse(); } else if (expectation == STRING) { assertThat(reader.nextString()).isEqualTo("string"); } else if (expectation == NUMBER) { assertThat(reader.nextInt()).isEqualTo(123); } else if (expectation == NULL) { reader.nextNull(); } else if (expectation instanceof Class && Exception.class.isAssignableFrom((Class) expectation)) { try { reader.peek(); fail(); } catch (Exception expected) { assertEquals(expected.toString(), expectation, expected.getClass()); } } else { throw new AssertionError(); } } } @Test public void nextSourceObject_withWhitespace() throws IOException { // language=JSON JsonReader reader = newReader("{\n \"a\": {\n \"b\": 2,\n \"c\": 3\n }\n}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("{\n \"b\": 2,\n \"c\": 3\n }"); } } @Test public void nextSourceLong_WithWhitespace() throws IOException { // language=JSON JsonReader reader = newReader("{\n \"a\": -2\n}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("-2"); } } /** * Confirm that {@link JsonReader#nextSource} doesn't load data from the underlying stream until * its required by the caller. If the source is backed by a slow network stream, we want users to * get data as it arrives. * *

Because we don't have a slow stream in this test, we just add bytes to our underlying stream * immediately before they're needed. */ @Test public void nextSourceStreams() throws IOException { Buffer stream = new Buffer(); stream.writeUtf8("[\""); JsonReader reader = JsonReader.of(Okio.buffer((Source) stream)); reader.beginArray(); BufferedSource source = reader.nextSource(); assertThat(source.readUtf8(1)).isEqualTo("\""); stream.writeUtf8("hello"); assertThat(source.readUtf8(5)).isEqualTo("hello"); stream.writeUtf8("world"); assertThat(source.readUtf8(5)).isEqualTo("world"); stream.writeUtf8("\""); assertThat(source.readUtf8(1)).isEqualTo("\""); stream.writeUtf8("]"); assertThat(source.exhausted()).isTrue(); reader.endArray(); } @Test public void nextSourceObjectAfterSelect() throws IOException { // language=JSON JsonReader reader = newReader("[\"p\u0065psi\"]"); reader.beginArray(); assertThat(reader.selectName(JsonReader.Options.of("coke"))).isEqualTo(-1); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("\"pepsi\""); // not the original characters! } } @Test public void nextSourceObjectAfterPromoteNameToValue() throws IOException { // language=JSON JsonReader reader = newReader("{\"a\":true}"); reader.beginObject(); reader.promoteNameToValue(); try (BufferedSource valueSource = reader.nextSource()) { assertThat(valueSource.readUtf8()).isEqualTo("\"a\""); } assertThat(reader.nextBoolean()).isEqualTo(true); reader.endObject(); } @Test public void nextSourcePath() throws IOException { // language=JSON JsonReader reader = newReader("{\"a\":true,\"b\":[],\"c\":false}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.getPath()).isEqualTo("$.a"); assertThat(reader.nextBoolean()).isTrue(); assertThat(reader.getPath()).isEqualTo("$.a"); assertThat(reader.nextName()).isEqualTo("b"); try (BufferedSource valueSource = reader.nextSource()) { assertThat(reader.getPath()).isEqualTo("$.b"); assertThat(valueSource.readUtf8()).isEqualTo("[]"); } assertThat(reader.getPath()).isEqualTo("$.b"); assertThat(reader.nextName()).isEqualTo("c"); assertThat(reader.getPath()).isEqualTo("$.c"); assertThat(reader.nextBoolean()).isFalse(); assertThat(reader.getPath()).isEqualTo("$.c"); reader.endObject(); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonUtf8WriterTest.java ================================================ /* * Copyright (C) 2010 Google 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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import java.io.IOException; import okio.Buffer; import org.junit.Test; public final class JsonUtf8WriterTest { @Test public void prettyPrintObject() throws IOException { Buffer buffer = new Buffer(); JsonWriter writer = JsonWriter.of(buffer); writer.setSerializeNulls(true); writer.setIndent(" "); writer.beginObject(); writer.name("a").value(true); writer.name("b").value(false); writer.name("c").value(5.0); writer.name("e").nullValue(); writer.name("f").beginArray(); writer.value(6.0); writer.value(7.0); writer.endArray(); writer.name("g").beginObject(); writer.name("h").value(8.0); writer.name("i").value(9.0); writer.endObject(); writer.endObject(); String expected = "{\n" + " \"a\": true,\n" + " \"b\": false,\n" + " \"c\": 5.0,\n" + " \"e\": null,\n" + " \"f\": [\n" + " 6.0,\n" + " 7.0\n" + " ],\n" + " \"g\": {\n" + " \"h\": 8.0,\n" + " \"i\": 9.0\n" + " }\n" + "}"; assertThat(buffer.readUtf8()).isEqualTo(expected); } @Test public void prettyPrintArray() throws IOException { Buffer buffer = new Buffer(); JsonWriter writer = JsonWriter.of(buffer); writer.setIndent(" "); writer.beginArray(); writer.value(true); writer.value(false); writer.value(5.0); writer.nullValue(); writer.beginObject(); writer.name("a").value(6.0); writer.name("b").value(7.0); writer.endObject(); writer.beginArray(); writer.value(8.0); writer.value(9.0); writer.endArray(); writer.endArray(); String expected = "[\n" + " true,\n" + " false,\n" + " 5.0,\n" + " null,\n" + " {\n" + " \"a\": 6.0,\n" + " \"b\": 7.0\n" + " },\n" + " [\n" + " 8.0,\n" + " 9.0\n" + " ]\n" + "]"; assertThat(buffer.readUtf8()).isEqualTo(expected); } @Test public void repeatedNameIgnored() throws IOException { Buffer buffer = new Buffer(); JsonWriter writer = JsonWriter.of(buffer); writer.beginObject(); writer.name("a").value(1); writer.name("a").value(2); writer.endObject(); // JsonWriter doesn't attempt to detect duplicate names assertThat(buffer.readUtf8()).isEqualTo("{\"a\":1,\"a\":2}"); } @Test public void valueFromSource() throws IOException { Buffer buffer = new Buffer(); JsonWriter writer = JsonWriter.of(buffer); writer.beginObject(); writer.name("a"); writer.value(new Buffer().writeUtf8("[\"value\"]")); writer.name("b"); writer.value(new Buffer().writeUtf8("2")); writer.name("c"); writer.value(3); writer.name("d"); writer.value(new Buffer().writeUtf8("null")); writer.endObject(); assertThat(buffer.readUtf8()).isEqualTo("{\"a\":[\"value\"],\"b\":2,\"c\":3,\"d\":null}"); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonValueReaderTest.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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static com.squareup.moshi.MoshiTesting.jsonValueReader; import static com.squareup.moshi.TestUtil.MAX_DEPTH; import static com.squareup.moshi.TestUtil.repeat; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.junit.Assert.fail; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.junit.Test; public final class JsonValueReaderTest { @Test public void array() throws Exception { List root = new ArrayList<>(); root.add("s"); root.add(1.5d); root.add(true); root.add(null); JsonReader reader = jsonValueReader(root); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_ARRAY); reader.beginArray(); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextString()).isEqualTo("s"); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NUMBER); assertThat(reader.nextDouble()).isEqualTo(1.5d); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BOOLEAN); assertThat(reader.nextBoolean()).isEqualTo(true); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NULL); assertThat(reader.nextNull()).isNull(); assertThat(reader.hasNext()).isFalse(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_ARRAY); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void object() throws Exception { Map root = new LinkedHashMap<>(); root.put("a", "s"); root.put("b", 1.5d); root.put("c", true); root.put("d", null); JsonReader reader = jsonValueReader(root); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); reader.beginObject(); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextString()).isEqualTo("s"); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); assertThat(reader.nextName()).isEqualTo("b"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NUMBER); assertThat(reader.nextDouble()).isEqualTo(1.5d); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); assertThat(reader.nextName()).isEqualTo("c"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BOOLEAN); assertThat(reader.nextBoolean()).isEqualTo(true); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); assertThat(reader.nextName()).isEqualTo("d"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NULL); assertThat(reader.nextNull()).isNull(); assertThat(reader.hasNext()).isFalse(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void nesting() throws Exception { List>>> root = singletonList(singletonMap("a", singletonList(singletonMap("b", 1.5d)))); JsonReader reader = jsonValueReader(root); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_ARRAY); reader.beginArray(); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); reader.beginObject(); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_ARRAY); reader.beginArray(); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); reader.beginObject(); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NAME); assertThat(reader.nextName()).isEqualTo("b"); assertThat(reader.hasNext()).isTrue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.NUMBER); assertThat(reader.nextDouble()).isEqualTo(1.5d); assertThat(reader.hasNext()).isFalse(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT); reader.endObject(); assertThat(reader.hasNext()).isFalse(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_ARRAY); reader.endArray(); assertThat(reader.hasNext()).isFalse(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT); reader.endObject(); assertThat(reader.hasNext()).isFalse(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_ARRAY); reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void promoteNameToValue() throws Exception { Map root = singletonMap("a", "b"); JsonReader reader = jsonValueReader(root); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextString()).isEqualTo("a"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextString()).isEqualTo("b"); reader.endObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void endArrayTooEarly() throws Exception { JsonReader reader = jsonValueReader(singletonList("s")); reader.beginArray(); try { reader.endArray(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected END_ARRAY but was s, a java.lang.String, at path $[0]"); } } @Test public void endObjectTooEarly() throws Exception { JsonReader reader = jsonValueReader(singletonMap("a", "b")); reader.beginObject(); try { reader.endObject(); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().startsWith("Expected END_OBJECT but was a=b"); } } @Test public void unsupportedType() throws Exception { JsonReader reader = jsonValueReader(singletonList(new StringBuilder("x"))); reader.beginArray(); try { reader.peek(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected a JSON value but was x, a java.lang.StringBuilder, at path $[0]"); } } @Test public void unsupportedKeyType() throws Exception { JsonReader reader = jsonValueReader(singletonMap(new StringBuilder("x"), "y")); reader.beginObject(); try { reader.nextName(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected NAME but was x, a java.lang.StringBuilder, at path $."); } } @Test public void nullKey() throws Exception { JsonReader reader = jsonValueReader(singletonMap(null, "y")); reader.beginObject(); try { reader.nextName(); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected NAME but was null at path $."); } } @Test public void unexpectedIntType() throws Exception { JsonReader reader = jsonValueReader(singletonList(new StringBuilder("1"))); reader.beginArray(); try { reader.nextInt(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected NUMBER but was 1, a java.lang.StringBuilder, at path $[0]"); } } @Test public void unexpectedLongType() throws Exception { JsonReader reader = jsonValueReader(singletonList(new StringBuilder("1"))); reader.beginArray(); try { reader.nextLong(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected NUMBER but was 1, a java.lang.StringBuilder, at path $[0]"); } } @Test public void unexpectedDoubleType() throws Exception { JsonReader reader = jsonValueReader(singletonList(new StringBuilder("1"))); reader.beginArray(); try { reader.nextDouble(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected NUMBER but was 1, a java.lang.StringBuilder, at path $[0]"); } } @Test public void unexpectedStringType() throws Exception { JsonReader reader = jsonValueReader(singletonList(new StringBuilder("s"))); reader.beginArray(); try { reader.nextString(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected STRING but was s, a java.lang.StringBuilder, at path $[0]"); } } @Test public void unexpectedBooleanType() throws Exception { JsonReader reader = jsonValueReader(singletonList(new StringBuilder("true"))); reader.beginArray(); try { reader.nextBoolean(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected BOOLEAN but was true, a java.lang.StringBuilder, at path $[0]"); } } @Test public void unexpectedNullType() throws Exception { JsonReader reader = jsonValueReader(singletonList(new StringBuilder("null"))); reader.beginArray(); try { reader.nextNull(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected NULL but was null, a java.lang.StringBuilder, at path $[0]"); } } @Test public void skipRoot() throws Exception { JsonReader reader = jsonValueReader(singletonList(new StringBuilder("x"))); reader.skipValue(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void skipListValue() throws Exception { List root = new ArrayList<>(); root.add("a"); root.add("b"); root.add("c"); JsonReader reader = jsonValueReader(root); reader.beginArray(); assertThat(reader.getPath()).isEqualTo("$[0]"); assertThat(reader.nextString()).isEqualTo("a"); assertThat(reader.getPath()).isEqualTo("$[1]"); reader.skipValue(); assertThat(reader.getPath()).isEqualTo("$[2]"); assertThat(reader.nextString()).isEqualTo("c"); reader.endArray(); } @Test public void skipObjectName() throws Exception { Map root = new LinkedHashMap<>(); root.put("a", "s"); root.put("b", 1.5d); root.put("c", true); JsonReader reader = jsonValueReader(root); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.getPath()).isEqualTo("$.a"); assertThat(reader.nextString()).isEqualTo("s"); assertThat(reader.getPath()).isEqualTo("$.a"); reader.skipValue(); assertThat(reader.getPath()).isEqualTo("$.null"); assertThat(reader.nextDouble()).isEqualTo(1.5d); assertThat(reader.getPath()).isEqualTo("$.null"); assertThat(reader.nextName()).isEqualTo("c"); assertThat(reader.getPath()).isEqualTo("$.c"); assertThat(reader.nextBoolean()).isEqualTo(true); assertThat(reader.getPath()).isEqualTo("$.c"); reader.endObject(); } @Test public void skipObjectValue() throws Exception { Map root = new LinkedHashMap<>(); root.put("a", "s"); root.put("b", 1.5d); root.put("c", true); JsonReader reader = jsonValueReader(root); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.getPath()).isEqualTo("$.a"); assertThat(reader.nextString()).isEqualTo("s"); assertThat(reader.getPath()).isEqualTo("$.a"); assertThat(reader.nextName()).isEqualTo("b"); assertThat(reader.getPath()).isEqualTo("$.b"); reader.skipValue(); assertThat(reader.getPath()).isEqualTo("$.null"); assertThat(reader.nextName()).isEqualTo("c"); assertThat(reader.getPath()).isEqualTo("$.c"); assertThat(reader.nextBoolean()).isEqualTo(true); assertThat(reader.getPath()).isEqualTo("$.c"); reader.endObject(); } @Test public void failOnUnknown() throws Exception { JsonReader reader = jsonValueReader(singletonList("a")); reader.setFailOnUnknown(true); reader.beginArray(); try { reader.skipValue(); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Cannot skip unexpected STRING at $[0]"); } } @Test public void close() throws Exception { try { JsonReader reader = jsonValueReader(singletonList("a")); reader.beginArray(); reader.close(); reader.nextString(); fail(); } catch (IllegalStateException expected) { } try { JsonReader reader = jsonValueReader(singletonList("a")); reader.close(); reader.beginArray(); fail(); } catch (IllegalStateException expected) { } } @Test public void numberToStringCoersion() throws Exception { JsonReader reader = jsonValueReader(Arrays.asList(0, 9223372036854775807L, 2.5d, 3.01f, "a", "5")); reader.beginArray(); assertThat(reader.nextString()).isEqualTo("0"); assertThat(reader.nextString()).isEqualTo("9223372036854775807"); assertThat(reader.nextString()).isEqualTo("2.5"); assertThat(reader.nextString()).isEqualTo("3.01"); assertThat(reader.nextString()).isEqualTo("a"); assertThat(reader.nextString()).isEqualTo("5"); reader.endArray(); } @Test public void tooDeeplyNestedArrays() throws IOException { Object root = Collections.emptyList(); for (int i = 0; i < MAX_DEPTH + 1; i++) { root = singletonList(root); } JsonReader reader = jsonValueReader(root); for (int i = 0; i < MAX_DEPTH; i++) { reader.beginArray(); } try { reader.beginArray(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Nesting too deep at $" + repeat("[0]", MAX_DEPTH + 1)); } } @Test public void tooDeeplyNestedObjects() throws IOException { Object root = Boolean.TRUE; for (int i = 0; i < MAX_DEPTH + 1; i++) { root = singletonMap("a", root); } JsonReader reader = jsonValueReader(root); for (int i = 0; i < MAX_DEPTH; i++) { reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); } try { reader.beginObject(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Nesting too deep at $" + repeat(".a", MAX_DEPTH) + "."); } } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonValueWriterTest.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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static com.squareup.moshi.MoshiTesting.jsonValueWriter; import static com.squareup.moshi.MoshiTesting.root; import static java.util.Collections.singletonList; import static org.junit.Assert.fail; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import okio.Buffer; import org.junit.Test; public final class JsonValueWriterTest { @SuppressWarnings("unchecked") @Test public void array() throws Exception { JsonWriter writer = jsonValueWriter(); writer.beginArray(); writer.value("s"); writer.value(1.5d); writer.value(true); writer.nullValue(); writer.endArray(); assertThat((List) root(writer)).containsExactly("s", 1.5d, true, null); } @Test public void object() throws Exception { JsonWriter writer = jsonValueWriter(); writer.setSerializeNulls(true); writer.beginObject(); writer.name("a").value("s"); writer.name("b").value(1.5d); writer.name("c").value(true); writer.name("d").nullValue(); writer.endObject(); assertThat((Map) root(writer)) .containsExactly("a", "s", "b", 1.5d, "c", true, "d", null); } @Test public void repeatedNameThrows() throws IOException { JsonWriter writer = jsonValueWriter(); writer.beginObject(); writer.name("a").value(1L); try { writer.name("a").value(2L); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Map key 'a' has multiple values at path $.a: 1 and 2"); } } @Test public void valueLongEmitsLong() throws Exception { JsonWriter writer = jsonValueWriter(); writer.beginArray(); writer.value(Long.MIN_VALUE); writer.value(-1L); writer.value(0L); writer.value(1L); writer.value(Long.MAX_VALUE); writer.endArray(); List numbers = Arrays.asList(Long.MIN_VALUE, -1L, 0L, 1L, Long.MAX_VALUE); assertThat((List) root(writer)).isEqualTo(numbers); } @Test public void valueDoubleEmitsDouble() throws Exception { JsonWriter writer = jsonValueWriter(); writer.setLenient(true); writer.beginArray(); writer.value(-2147483649.0d); writer.value(-2147483648.0d); writer.value(-1.0d); writer.value(0.0d); writer.value(1.0d); writer.value(2147483647.0d); writer.value(2147483648.0d); writer.value(9007199254740991.0d); writer.value(9007199254740992.0d); writer.value(9007199254740994.0d); writer.value(9223372036854776832.0d); writer.value(-0.5d); writer.value(-0.0d); writer.value(0.5d); writer.value(9.22337203685478e18); writer.value(Double.NEGATIVE_INFINITY); writer.value(Double.MIN_VALUE); writer.value(Double.MIN_NORMAL); writer.value(-Double.MIN_NORMAL); writer.value(Double.MAX_VALUE); writer.value(Double.POSITIVE_INFINITY); writer.value(Double.NaN); writer.endArray(); List numbers = Arrays.asList( -2147483649.0d, -2147483648.0d, -1.0d, 0.0d, 1.0d, 2147483647.0d, 2147483648.0d, 9007199254740991.0d, 9007199254740992.0d, 9007199254740994.0d, 9223372036854775807.0d, -0.5d, -0.0d, 0.5d, 9.22337203685478e18, Double.NEGATIVE_INFINITY, Double.MIN_VALUE, Double.MIN_NORMAL, -Double.MIN_NORMAL, Double.MAX_VALUE, Double.POSITIVE_INFINITY, Double.NaN); assertThat((List) root(writer)).isEqualTo(numbers); } @Test public void primitiveIntegerTypesEmitLong() throws Exception { JsonWriter writer = jsonValueWriter(); writer.beginArray(); writer.value(Byte.valueOf(Byte.MIN_VALUE)); writer.value(Short.valueOf(Short.MIN_VALUE)); writer.value(Integer.valueOf(Integer.MIN_VALUE)); writer.value(Long.valueOf(Long.MIN_VALUE)); writer.endArray(); List numbers = Arrays.asList(-128L, -32768L, -2147483648L, -9223372036854775808L); assertThat((List) root(writer)).isEqualTo(numbers); } @Test public void primitiveFloatingPointTypesEmitDouble() throws Exception { JsonWriter writer = jsonValueWriter(); writer.beginArray(); writer.value(Float.valueOf(0.5f)); writer.value(Double.valueOf(0.5d)); writer.endArray(); List numbers = Arrays.asList(0.5d, 0.5d); assertThat((List) root(writer)).isEqualTo(numbers); } @Test public void otherNumberTypesEmitBigDecimal() throws Exception { JsonWriter writer = jsonValueWriter(); writer.beginArray(); writer.value(new AtomicInteger(-2147483648)); writer.value(new AtomicLong(-9223372036854775808L)); writer.value(new BigInteger("-9223372036854775808")); writer.value(new BigInteger("-1")); writer.value(new BigInteger("0")); writer.value(new BigInteger("1")); writer.value(new BigInteger("9223372036854775807")); writer.value(new BigDecimal("-9223372036854775808")); writer.value(new BigDecimal("-1")); writer.value(new BigDecimal("0")); writer.value(new BigDecimal("1")); writer.value(new BigDecimal("9223372036854775807")); writer.value(new BigInteger("-9223372036854775809")); writer.value(new BigInteger("9223372036854775808")); writer.value(new BigDecimal("-9223372036854775809")); writer.value(new BigDecimal("9223372036854775808")); writer.value(new BigDecimal("0.5")); writer.value(new BigDecimal("100000e15")); writer.value(new BigDecimal("0.0000100e-10")); writer.endArray(); List numbers = Arrays.asList( new BigDecimal("-2147483648"), new BigDecimal("-9223372036854775808"), new BigDecimal("-9223372036854775808"), new BigDecimal("-1"), new BigDecimal("0"), new BigDecimal("1"), new BigDecimal("9223372036854775807"), new BigDecimal("-9223372036854775808"), new BigDecimal("-1"), new BigDecimal("0"), new BigDecimal("1"), new BigDecimal("9223372036854775807"), new BigDecimal("-9223372036854775809"), new BigDecimal("9223372036854775808"), new BigDecimal("-9223372036854775809"), new BigDecimal("9223372036854775808"), new BigDecimal("0.5"), new BigDecimal("100000e15"), new BigDecimal("0.0000100e-10")); assertThat((List) root(writer)).isEqualTo(numbers); } @Test public void valueCustomNumberTypeEmitsLongOrBigDecimal() throws Exception { JsonWriter writer = jsonValueWriter(); writer.beginArray(); writer.value(stringNumber("-9223372036854775809")); writer.value(stringNumber("-9223372036854775808")); writer.value(stringNumber("0.5")); writer.value(stringNumber("1.0")); writer.endArray(); List numbers = Arrays.asList( new BigDecimal("-9223372036854775809"), new BigDecimal("-9223372036854775808"), new BigDecimal("0.5"), new BigDecimal("1.0")); assertThat((List) root(writer)).isEqualTo(numbers); } @Test public void valueFromSource() throws IOException { JsonWriter writer = jsonValueWriter(); writer.beginObject(); writer.name("a"); writer.value(new Buffer().writeUtf8("[\"value\"]")); writer.name("b"); writer.value(new Buffer().writeUtf8("2")); writer.name("c"); writer.value(3); writer.name("d"); writer.value(new Buffer().writeUtf8("null")); writer.endObject(); assertThat((Map) root(writer)) .containsExactly("a", singletonList("value"), "b", 2.0d, "c", 3L, "d", null); } /** * Returns an instance of number whose {@link #toString} is {@code s}. Using the standard number * methods like {@link Number#doubleValue} are awkward because they may truncate or discard * precision. */ private Number stringNumber(final String s) { return new Number() { @Override public int intValue() { throw new AssertionError(); } @Override public long longValue() { throw new AssertionError(); } @Override public float floatValue() { throw new AssertionError(); } @Override public double doubleValue() { throw new AssertionError(); } @Override public String toString() { return s; } }; } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonWriterPathTest.java ================================================ /* * Copyright (C) 2014 Google 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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assume.assumeTrue; import java.io.IOException; import java.math.BigInteger; import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public final class JsonWriterPathTest { @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { return JsonCodecFactory.factories(); } @Test public void path() throws IOException { JsonWriter writer = factory.newWriter(); assertThat(writer.getPath()).isEqualTo("$"); writer.beginObject(); assertThat(writer.getPath()).isEqualTo("$."); writer.name("a"); assertThat(writer.getPath()).isEqualTo("$.a"); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$.a[0]"); writer.value(2); assertThat(writer.getPath()).isEqualTo("$.a[1]"); writer.value(true); assertThat(writer.getPath()).isEqualTo("$.a[2]"); writer.value(false); assertThat(writer.getPath()).isEqualTo("$.a[3]"); writer.nullValue(); assertThat(writer.getPath()).isEqualTo("$.a[4]"); writer.value("b"); assertThat(writer.getPath()).isEqualTo("$.a[5]"); writer.beginObject(); assertThat(writer.getPath()).isEqualTo("$.a[5]."); writer.name("c"); assertThat(writer.getPath()).isEqualTo("$.a[5].c"); writer.value("d"); assertThat(writer.getPath()).isEqualTo("$.a[5].c"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$.a[6]"); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$.a[6][0]"); writer.value(3); assertThat(writer.getPath()).isEqualTo("$.a[6][1]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$.a[7]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$.a"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); } @Test public void arrayOfObjects() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$[0]"); writer.beginObject(); assertThat(writer.getPath()).isEqualTo("$[0]."); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$[1]"); writer.beginObject(); assertThat(writer.getPath()).isEqualTo("$[1]."); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$[2]"); writer.beginObject(); assertThat(writer.getPath()).isEqualTo("$[2]."); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$[3]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$"); } @Test public void arrayOfArrays() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$[0]"); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$[0][0]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$[1]"); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$[1][0]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$[2]"); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$[2][0]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$[3]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$"); } @Test public void objectPath() throws IOException { JsonWriter writer = factory.newWriter(); assertThat(writer.getPath()).isEqualTo("$"); writer.beginObject(); assertThat(writer.getPath()).isEqualTo("$."); writer.name("a"); assertThat(writer.getPath()).isEqualTo("$.a"); writer.value(1); assertThat(writer.getPath()).isEqualTo("$.a"); writer.name("b"); assertThat(writer.getPath()).isEqualTo("$.b"); writer.value(2); assertThat(writer.getPath()).isEqualTo("$.b"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); writer.close(); assertThat(writer.getPath()).isEqualTo("$"); } @Test public void nestedObjects() throws IOException { JsonWriter writer = factory.newWriter(); assertThat(writer.getPath()).isEqualTo("$"); writer.beginObject(); assertThat(writer.getPath()).isEqualTo("$."); writer.name("a"); assertThat(writer.getPath()).isEqualTo("$.a"); writer.beginObject(); assertThat(writer.getPath()).isEqualTo("$.a."); writer.name("b"); assertThat(writer.getPath()).isEqualTo("$.a.b"); writer.beginObject(); assertThat(writer.getPath()).isEqualTo("$.a.b."); writer.name("c"); assertThat(writer.getPath()).isEqualTo("$.a.b.c"); writer.nullValue(); assertThat(writer.getPath()).isEqualTo("$.a.b.c"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$.a.b"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$.a"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); } @Test public void arrayPath() throws IOException { JsonWriter writer = factory.newWriter(); assertThat(writer.getPath()).isEqualTo("$"); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$[0]"); writer.value(1); assertThat(writer.getPath()).isEqualTo("$[1]"); writer.value(true); assertThat(writer.getPath()).isEqualTo("$[2]"); writer.value("a"); assertThat(writer.getPath()).isEqualTo("$[3]"); writer.value(5.5d); assertThat(writer.getPath()).isEqualTo("$[4]"); writer.value(BigInteger.ONE); assertThat(writer.getPath()).isEqualTo("$[5]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$"); writer.close(); assertThat(writer.getPath()).isEqualTo("$"); } @Test public void nestedArrays() throws IOException { JsonWriter writer = factory.newWriter(); assertThat(writer.getPath()).isEqualTo("$"); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$[0]"); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$[0][0]"); writer.beginArray(); assertThat(writer.getPath()).isEqualTo("$[0][0][0]"); writer.nullValue(); assertThat(writer.getPath()).isEqualTo("$[0][0][1]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$[0][1]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$[1]"); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$"); writer.close(); assertThat(writer.getPath()).isEqualTo("$"); } @Test public void multipleTopLevelValuesInOneDocument() throws IOException { assumeTrue(factory.encodesToBytes()); JsonWriter writer = factory.newWriter(); writer.setLenient(true); writer.beginArray(); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$"); writer.beginArray(); writer.endArray(); assertThat(writer.getPath()).isEqualTo("$"); } @Test public void skipNulls() throws IOException { JsonWriter writer = factory.newWriter(); writer.setSerializeNulls(false); assertThat(writer.getPath()).isEqualTo("$"); writer.beginObject(); assertThat(writer.getPath()).isEqualTo("$."); writer.name("a"); assertThat(writer.getPath()).isEqualTo("$.a"); writer.nullValue(); assertThat(writer.getPath()).isEqualTo("$.a"); writer.name("b"); assertThat(writer.getPath()).isEqualTo("$.b"); writer.nullValue(); assertThat(writer.getPath()).isEqualTo("$.b"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/JsonWriterTest.java ================================================ /* * Copyright (C) 2010 Google 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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static com.squareup.moshi.TestUtil.MAX_DEPTH; import static com.squareup.moshi.TestUtil.repeat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import java.io.IOException; import java.math.BigDecimal; import java.math.BigInteger; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import okio.BufferedSink; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public final class JsonWriterTest { @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { return JsonCodecFactory.factories(); } @Test public void nullsValuesNotSerializedByDefault() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); writer.nullValue(); writer.endObject(); writer.close(); assertThat(factory.json()).isEqualTo("{}"); } @Test public void nullsValuesSerializedWhenConfigured() throws IOException { JsonWriter writer = factory.newWriter(); writer.setSerializeNulls(true); writer.beginObject(); writer.name("a"); writer.nullValue(); writer.endObject(); writer.close(); assertThat(factory.json()).isEqualTo("{\"a\":null}"); } @Test public void topLevelBoolean() throws IOException { JsonWriter writer = factory.newWriter(); writer.value(true); writer.close(); assertThat(factory.json()).isEqualTo("true"); } @Test public void topLevelNull() throws IOException { JsonWriter writer = factory.newWriter(); writer.nullValue(); writer.close(); assertThat(factory.json()).isEqualTo("null"); } @Test public void topLevelInt() throws IOException { JsonWriter writer = factory.newWriter(); writer.value(123); writer.close(); assertThat(factory.json()).isEqualTo("123"); } @Test public void topLevelDouble() throws IOException { JsonWriter writer = factory.newWriter(); writer.value(123.4); writer.close(); assertThat(factory.json()).isEqualTo("123.4"); } @Test public void topLevelString() throws IOException { JsonWriter writer = factory.newWriter(); writer.value("a"); writer.close(); assertThat(factory.json()).isEqualTo("\"a\""); } @Test public void invalidTopLevelTypes() throws IOException { JsonWriter writer = factory.newWriter(); try { writer.name("hello").value("world"); fail(); } catch (IllegalStateException expected) { } } @Test public void twoNames() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); try { writer.name("a"); fail(); } catch (IllegalStateException expected) { } } @Test public void nameWithoutValue() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); try { writer.endObject(); fail(); } catch (IllegalStateException expected) { } } @Test public void valueWithoutName() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); try { writer.value(true); fail(); } catch (IllegalStateException expected) { } } @Test public void multipleTopLevelValues() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray().endArray(); try { writer.beginArray(); fail(); } catch (IllegalStateException expected) { } } @Test public void badNestingObject() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.beginObject(); try { writer.endArray(); fail(); } catch (IllegalStateException expected) { } } @Test public void badNestingArray() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.beginArray(); try { writer.endObject(); fail(); } catch (IllegalStateException expected) { } } @Test public void nullName() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); try { writer.name(null); fail(); } catch (NullPointerException expected) { } } @Test public void nullStringValue() throws IOException { JsonWriter writer = factory.newWriter(); writer.setSerializeNulls(true); writer.beginObject(); writer.name("a"); writer.value((String) null); writer.endObject(); assertThat(factory.json()).isEqualTo("{\"a\":null}"); } @Test public void nonFiniteDoubles() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); try { writer.value(Double.NaN); fail(); } catch (IllegalArgumentException expected) { } try { writer.value(Double.NEGATIVE_INFINITY); fail(); } catch (IllegalArgumentException expected) { } try { writer.value(Double.POSITIVE_INFINITY); fail(); } catch (IllegalArgumentException expected) { } } @Test public void nonFiniteBoxedDoubles() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); try { writer.value(Double.valueOf(Double.NaN)); fail(); } catch (IllegalArgumentException expected) { } try { writer.value(Double.valueOf(Double.NEGATIVE_INFINITY)); fail(); } catch (IllegalArgumentException expected) { } try { writer.value(Double.valueOf(Double.POSITIVE_INFINITY)); fail(); } catch (IllegalArgumentException expected) { } } @Test public void doubles() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.value(-0.0); writer.value(1.0); writer.value(Double.MAX_VALUE); writer.value(Double.MIN_VALUE); writer.value(0.0); writer.value(-0.5); writer.value(2.2250738585072014E-308); writer.value(Math.PI); writer.value(Math.E); writer.endArray(); writer.close(); assertThat(factory.json()) .isEqualTo( "[-0.0," + "1.0," + "1.7976931348623157E308," + "4.9E-324," + "0.0," + "-0.5," + "2.2250738585072014E-308," + "3.141592653589793," + "2.718281828459045]"); } @Test public void longs() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.value(0); writer.value(1); writer.value(-1); writer.value(Long.MIN_VALUE); writer.value(Long.MAX_VALUE); writer.endArray(); writer.close(); assertThat(factory.json()) .isEqualTo("[0," + "1," + "-1," + "-9223372036854775808," + "9223372036854775807]"); } @Test public void numbers() throws IOException { assumeTrue(factory.supportsBigNumbers()); JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.value(new BigInteger("0")); writer.value(new BigInteger("9223372036854775808")); writer.value(new BigInteger("-9223372036854775809")); writer.value(new BigDecimal("3.141592653589793238462643383")); writer.endArray(); writer.close(); assertThat(factory.json()) .isEqualTo( "[0," + "9223372036854775808," + "-9223372036854775809," + "3.141592653589793238462643383]"); } @Test public void nullNumbers() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.value((Number) null); writer.endArray(); writer.close(); assertThat(factory.json()).isEqualTo("[null]"); } @Test public void booleans() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.value(true); writer.value(false); writer.endArray(); assertThat(factory.json()).isEqualTo("[true,false]"); } @Test public void boxedBooleans() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.value((Boolean) true); writer.value((Boolean) false); writer.value((Boolean) null); writer.endArray(); assertThat(factory.json()).isEqualTo("[true,false,null]"); } @Test public void nulls() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.nullValue(); writer.endArray(); assertThat(factory.json()).isEqualTo("[null]"); } @Test public void strings() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.value("a"); writer.value("a\""); writer.value("\""); writer.value(":"); writer.value(","); writer.value("\b"); writer.value("\f"); writer.value("\n"); writer.value("\r"); writer.value("\t"); writer.value(" "); writer.value("\\"); writer.value("{"); writer.value("}"); writer.value("["); writer.value("]"); writer.value("\0"); writer.value("\u0019"); writer.endArray(); assertThat(factory.json()) .isEqualTo( "[\"a\"," + "\"a\\\"\"," + "\"\\\"\"," + "\":\"," + "\",\"," + "\"\\b\"," + "\"\\f\"," + "\"\\n\"," + "\"\\r\"," + "\"\\t\"," + "\" \"," + "\"\\\\\"," + "\"{\"," + "\"}\"," + "\"[\"," + "\"]\"," + "\"\\u0000\"," + "\"\\u0019\"]"); } @Test public void unicodeLineBreaksEscaped() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.value("\u2028 \u2029"); writer.endArray(); assertThat(factory.json()).isEqualTo("[\"\\u2028 \\u2029\"]"); } @Test public void emptyArray() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.endArray(); assertThat(factory.json()).isEqualTo("[]"); } @Test public void emptyObject() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.endObject(); assertThat(factory.json()).isEqualTo("{}"); } @Test public void objectsInArrays() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.beginObject(); writer.name("a").value(5); writer.name("b").value(false); writer.endObject(); writer.beginObject(); writer.name("c").value(6); writer.name("d").value(true); writer.endObject(); writer.endArray(); assertThat(factory.json()).isEqualTo("[{\"a\":5,\"b\":false}," + "{\"c\":6,\"d\":true}]"); } @Test public void arraysInObjects() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); writer.beginArray(); writer.value(5); writer.value(false); writer.endArray(); writer.name("b"); writer.beginArray(); writer.value(6); writer.value(true); writer.endArray(); writer.endObject(); assertThat(factory.json()).isEqualTo("{\"a\":[5,false]," + "\"b\":[6,true]}"); } @Test public void deepNestingArrays() throws IOException { JsonWriter writer = factory.newWriter(); for (int i = 0; i < MAX_DEPTH; i++) { writer.beginArray(); } for (int i = 0; i < MAX_DEPTH; i++) { writer.endArray(); } assertThat(factory.json()).isEqualTo(repeat("[", MAX_DEPTH) + repeat("]", MAX_DEPTH)); } @Test public void tooDeeplyNestingArrays() throws IOException { JsonWriter writer = factory.newWriter(); for (int i = 0; i < MAX_DEPTH; i++) { writer.beginArray(); } try { writer.beginArray(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Nesting too deep at $" + repeat("[0]", MAX_DEPTH) + ": circular reference?"); } } @Test public void deepNestingObjects() throws IOException { JsonWriter writer = factory.newWriter(); for (int i = 0; i < MAX_DEPTH; i++) { writer.beginObject(); writer.name("a"); } writer.value(true); for (int i = 0; i < MAX_DEPTH; i++) { writer.endObject(); } assertThat(factory.json()) .isEqualTo(repeat("{\"a\":", MAX_DEPTH) + "true" + repeat("}", MAX_DEPTH)); } @Test public void tooDeeplyNestingObjects() throws IOException { JsonWriter writer = factory.newWriter(); for (int i = 0; i < MAX_DEPTH; i++) { writer.beginObject(); writer.name("a"); } try { writer.beginObject(); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Nesting too deep at $" + repeat(".a", MAX_DEPTH) + ": circular reference?"); } } @Test public void lenientWriterPermitsMultipleTopLevelValues() throws IOException { assumeTrue(factory.encodesToBytes()); JsonWriter writer = factory.newWriter(); writer.setLenient(true); writer.beginArray(); writer.endArray(); writer.beginArray(); writer.endArray(); writer.close(); assertThat(factory.json()).isEqualTo("[][]"); } @Test public void strictWriterDoesNotPermitMultipleTopLevelValues() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.endArray(); try { writer.beginArray(); fail(); } catch (IllegalStateException expected) { } } @Test public void closedWriterThrowsOnStructure() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.endArray(); writer.close(); try { writer.beginArray(); fail(); } catch (IllegalStateException expected) { } try { writer.endArray(); fail(); } catch (IllegalStateException expected) { } try { writer.beginObject(); fail(); } catch (IllegalStateException expected) { } try { writer.endObject(); fail(); } catch (IllegalStateException expected) { } } @Test public void closedWriterThrowsOnName() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.endArray(); writer.close(); try { writer.name("a"); fail(); } catch (IllegalStateException expected) { } } @Test public void closedWriterThrowsOnValue() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.endArray(); writer.close(); try { writer.value("a"); fail(); } catch (IllegalStateException expected) { } } @Test public void closedWriterThrowsOnFlush() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.endArray(); writer.close(); try { writer.flush(); fail(); } catch (IllegalStateException expected) { } } @Test public void writerCloseIsIdempotent() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.endArray(); writer.close(); writer.close(); } @Test public void nameNotInObjectFails() throws IOException { JsonWriter writer = factory.newWriter(); try { writer.name("a"); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Nesting problem."); } } @Test public void missingValueInObjectIsANestingProblem() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); try { writer.name("b"); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Nesting problem."); } } @Test public void nameInArrayIsANestingProblem() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); try { writer.name("a"); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Nesting problem."); } } @Test public void danglingNameFails() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); try { writer.endObject(); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Dangling name: a"); } } @Test public void streamingValueInObject() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); BufferedSink value = writer.valueSink(); value.writeByte('"'); value.writeHexadecimalUnsignedLong(-1L); value.writeUtf8("sup"); value.writeDecimalLong(-1L); value.writeByte('"'); value.close(); writer.endObject(); assertThat(factory.json()).isEqualTo("{\"a\":\"ffffffffffffffffsup-1\"}"); } @Test public void streamingValueInArray() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.valueSink().writeByte('"').writeHexadecimalUnsignedLong(-1L).writeByte('"').close(); writer.valueSink().writeByte('"').writeUtf8("sup").writeByte('"').close(); writer.valueSink().writeUtf8("-1.0").close(); writer.endArray(); assertThat(factory.json()).isEqualTo("[\"ffffffffffffffff\",\"sup\",-1.0]"); } @Test public void streamingValueTopLevel() throws IOException { JsonWriter writer = factory.newWriter(); writer.valueSink().writeUtf8("-1.0").close(); assertThat(factory.json()).isEqualTo("-1.0"); } @Test public void streamingValueTwiceBeforeCloseFails() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); BufferedSink sink = writer.valueSink(); try { writer.valueSink(); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessageThat().isEqualTo("Sink from valueSink() was not closed"); } } @Test public void streamingValueTwiceAfterCloseFails() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); writer.valueSink().writeByte('0').close(); try { // TODO currently UTF-8 fails eagerly on valueSink() but value does not fail until close(). writer.valueSink().writeByte('0').close(); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessageThat().isEqualTo("Nesting problem."); } } @Test public void streamingValueAndScalarValueFails() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); BufferedSink sink = writer.valueSink(); try { writer.value("b"); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessageThat().isEqualTo("Sink from valueSink() was not closed"); } } @Test public void streamingValueAndNameFails() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); BufferedSink sink = writer.valueSink(); try { writer.name("b"); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessageThat().isEqualTo("Nesting problem."); } } @Test public void streamingValueInteractionAfterCloseFails() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); BufferedSink sink = writer.valueSink(); sink.writeUtf8("1.0"); sink.close(); try { sink.writeByte('1'); fail(); } catch (IllegalStateException e) { assertThat(e).hasMessageThat().isEqualTo("closed"); } } @Test public void streamingValueCloseIsIdempotent() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); BufferedSink sink = writer.valueSink(); sink.writeUtf8("1.0"); sink.close(); sink.close(); writer.endObject(); sink.close(); assertThat(factory.json()).isEqualTo("{\"a\":1.0}"); sink.close(); } @Test public void jsonValueTypes() throws IOException { JsonWriter writer = factory.newWriter(); writer.setSerializeNulls(true); writer.beginArray(); writer.jsonValue(null); writer.jsonValue(1.1d); writer.jsonValue(1L); writer.jsonValue(1); writer.jsonValue(true); writer.jsonValue("one"); writer.jsonValue(Collections.emptyList()); writer.jsonValue(Arrays.asList(1, 2, null, 3)); writer.jsonValue(Collections.emptyMap()); Map map = new LinkedHashMap<>(); map.put("one", "uno"); map.put("two", null); writer.jsonValue(map); writer.endArray(); assertThat(factory.json()) .isEqualTo( "[" + "null," + "1.1," + "1," + "1," + "true," + "\"one\"," + "[]," + "[1,2,null,3]," + "{}," + "{\"one\":\"uno\",\"two\":null}" + "]"); } @Test public void jsonValueIllegalTypes() throws IOException { try { factory.newWriter().jsonValue(new Object()); fail(); } catch (IllegalArgumentException e) { assertThat(e).hasMessageThat().isEqualTo("Unsupported type: java.lang.Object"); } try { factory.newWriter().jsonValue('1'); fail(); } catch (IllegalArgumentException e) { assertThat(e).hasMessageThat().isEqualTo("Unsupported type: java.lang.Character"); } Map mapWrongKey = new LinkedHashMap<>(); mapWrongKey.put(1, "one"); try { factory.newWriter().jsonValue(mapWrongKey); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo("Map keys must be of type String: java.lang.Integer"); } Map mapNullKey = new LinkedHashMap<>(); mapNullKey.put(null, "one"); try { factory.newWriter().jsonValue(mapNullKey); fail(); } catch (IllegalArgumentException e) { assertThat(e).hasMessageThat().isEqualTo("Map keys must be non-null"); } } @Test public void promoteStringNameToValue() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); writer.value("a"); writer.value("b"); writer.endObject(); assertThat(factory.json()).isEqualTo("{\"a\":\"b\"}"); } @Test public void promoteDoubleNameToValue() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); writer.value(5.0); writer.value("b"); writer.endObject(); assertThat(factory.json()).isEqualTo("{\"5.0\":\"b\"}"); } @Test public void promoteLongNameToValue() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); writer.value(5L); writer.value("b"); writer.endObject(); assertThat(factory.json()).isEqualTo("{\"5\":\"b\"}"); } @Test public void promoteNumberNameToValue() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); writer.value(BigInteger.ONE); writer.value("b"); writer.endObject(); assertThat(factory.json()).isEqualTo("{\"1\":\"b\"}"); } @Test public void promoteNullNameToValue() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); try { writer.nullValue(); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("null cannot be used as a map key in JSON at path $."); } } @Test public void promoteBooleanNameToValue() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); try { writer.value(true); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Boolean cannot be used as a map key in JSON at path $."); } } @Test public void promoteNameToValueCannotBeWrittenAsName() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); try { writer.name("a"); fail(); } catch (IllegalStateException expected) { assertThat(expected).hasMessageThat().isEqualTo("Nesting problem."); } } @Test public void promoteNameToValueAtEndOfObject() throws IOException { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); writer.endObject(); assertThat(factory.json()).isEqualTo("{}"); } @SuppressWarnings("rawtypes") @Test public void tags() throws IOException { JsonWriter writer = factory.newWriter(); assertThat(writer.tag(Integer.class)).isNull(); assertThat(writer.tag(CharSequence.class)).isNull(); writer.setTag(Integer.class, 1); writer.setTag(CharSequence.class, "Foo"); try { writer.setTag((Class) CharSequence.class, 1); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Tag value must be of type java.lang.CharSequence"); } Object intTag = writer.tag(Integer.class); assertThat(intTag).isEqualTo(1); assertThat(intTag).isInstanceOf(Integer.class); Object charSequenceTag = writer.tag(CharSequence.class); assertThat(charSequenceTag).isEqualTo("Foo"); assertThat(charSequenceTag).isInstanceOf(String.class); assertThat(writer.tag(String.class)).isNull(); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/KotlinExtensionsTest.kt ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.reflect.typeOf import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test @JsonQualifier @Retention(RUNTIME) annotation class TestAnnotation1 @JsonQualifier @Retention(RUNTIME) annotation class TestAnnotation2 @TestAnnotation1 @TestAnnotation2 class KotlinExtensionsTest { @Test fun nextAnnotationsShouldWork() { val annotations = KotlinExtensionsTest::class.java.annotations.filterTo(mutableSetOf()) { it.annotationClass.java.isAnnotationPresent(JsonQualifier::class.java) } assertEquals(2, annotations.size) val next = annotations.nextAnnotations() checkNotNull(next) assertEquals(1, next.size) assertTrue(next.first() is TestAnnotation1) } @Test fun arrayType() { val stringArray = String::class.asArrayType() check(stringArray.genericComponentType == String::class.java) val stringListType = typeOf>() val stringListArray = stringListType.asArrayType() val expected = Types.arrayOf(Types.newParameterizedType(List::class.java, String::class.java)) assertEquals(stringListArray, expected) } @Test fun addAdapterInferred() { // An adapter that always returns -1 val customIntdapter = object : JsonAdapter() { override fun fromJson(reader: JsonReader): Int { reader.skipValue() return -1 } override fun toJson(writer: JsonWriter, value: Int) { throw NotImplementedError() } } val moshi = Moshi.Builder().addAdapter(customIntdapter).build() assertEquals(-1, moshi.adapter().fromJson("5")) } @Test fun addAdapterInferred_parameterized() { // An adapter that always returns listOf(-1) val customIntListAdapter = object : JsonAdapter>() { override fun fromJson(reader: JsonReader): List { reader.skipValue() return listOf(-1) } override fun toJson(writer: JsonWriter, value: List) { throw NotImplementedError() } } val moshi = Moshi.Builder().addAdapter(customIntListAdapter).build() assertEquals(listOf(-1), moshi.adapter>().fromJson("[5]")) } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/MoshiTest.java ================================================ /* * 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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static com.squareup.moshi.TestUtil.newReader; import static com.squareup.moshi.TestUtil.repeat; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.Assert.fail; import android.util.Pair; import com.squareup.moshi.internal.MapJsonAdapter; import com.squareup.moshi.internal.StandardJsonAdapters; import com.squareup.moshi.internal.Util; import java.io.File; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.UUID; import javax.annotation.Nullable; import javax.crypto.KeyGenerator; import kotlin.reflect.KType; import okio.Buffer; import org.junit.Test; @SuppressWarnings({"CheckReturnValue", "ResultOfMethodCallIgnored"}) public final class MoshiTest { @Test public void booleanAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(boolean.class).lenient(); assertThat(adapter.fromJson("true")).isTrue(); assertThat(adapter.fromJson("TRUE")).isTrue(); assertThat(adapter.toJson(true)).isEqualTo("true"); assertThat(adapter.fromJson("false")).isFalse(); assertThat(adapter.fromJson("FALSE")).isFalse(); assertThat(adapter.toJson(false)).isEqualTo("false"); // Nulls not allowed for boolean.class try { adapter.fromJson("null"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a boolean but was NULL at path $"); } try { adapter.toJson(null); fail(); } catch (NullPointerException expected) { } } @Test public void BooleanAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Boolean.class).lenient(); assertThat(adapter.fromJson("true")).isTrue(); assertThat(adapter.toJson(true)).isEqualTo("true"); assertThat(adapter.fromJson("false")).isFalse(); assertThat(adapter.toJson(false)).isEqualTo("false"); // Allow nulls for Boolean.class assertThat(adapter.fromJson("null")).isEqualTo(null); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void byteAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(byte.class).lenient(); assertThat(adapter.fromJson("1")).isEqualTo((byte) 1); assertThat(adapter.toJson((byte) -2)).isEqualTo("254"); // Canonical byte representation is unsigned, but parse the whole range -128..255 assertThat(adapter.fromJson("-128")).isEqualTo((byte) -128); assertThat(adapter.fromJson("128")).isEqualTo((byte) -128); assertThat(adapter.toJson((byte) -128)).isEqualTo("128"); assertThat(adapter.fromJson("255")).isEqualTo((byte) -1); assertThat(adapter.toJson((byte) -1)).isEqualTo("255"); assertThat(adapter.fromJson("127")).isEqualTo((byte) 127); assertThat(adapter.toJson((byte) 127)).isEqualTo("127"); try { adapter.fromJson("256"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a byte but was 256 at path $"); } try { adapter.fromJson("-129"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a byte but was -129 at path $"); } // Nulls not allowed for byte.class try { adapter.fromJson("null"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected an int but was NULL at path $"); } try { adapter.toJson(null); fail(); } catch (NullPointerException expected) { } } @Test public void ByteAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Byte.class).lenient(); assertThat(adapter.fromJson("1")).isEqualTo((byte) 1); assertThat(adapter.toJson((byte) -2)).isEqualTo("254"); // Allow nulls for Byte.class assertThat(adapter.fromJson("null")).isEqualTo(null); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void charAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(char.class).lenient(); assertThat(adapter.fromJson("\"a\"")).isEqualTo('a'); assertThat(adapter.fromJson("'a'")).isEqualTo('a'); assertThat(adapter.toJson('b')).isEqualTo("\"b\""); // Exhaustively test all valid characters. Use an int to loop so we can check termination. for (int i = 0; i <= Character.MAX_VALUE; ++i) { final char c = (char) i; String s; switch (c) { // TODO: make JsonWriter.REPLACEMENT_CHARS visible for testing? case '\"': s = "\\\""; break; case '\\': s = "\\\\"; break; case '\t': s = "\\t"; break; case '\b': s = "\\b"; break; case '\n': s = "\\n"; break; case '\r': s = "\\r"; break; case '\f': s = "\\f"; break; case '\u2028': s = "\\u2028"; break; case '\u2029': s = "\\u2029"; break; default: if (c <= 0x1f) { s = String.format("\\u%04x", (int) c); } else if (c >= Character.MIN_SURROGATE && c <= Character.MAX_SURROGATE) { // TODO: not handled properly; do we need to? continue; } else { s = String.valueOf(c); } break; } s = '"' + s + '"'; assertThat(adapter.toJson(c)).isEqualTo(s); assertThat(adapter.fromJson(s)).isEqualTo(c); } try { // Only a single character is allowed. adapter.fromJson("'ab'"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a char but was \"ab\" at path $"); } // Nulls not allowed for char.class try { adapter.fromJson("null"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a string but was NULL at path $"); } try { adapter.toJson(null); fail(); } catch (NullPointerException expected) { } } @Test public void CharacterAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Character.class).lenient(); assertThat(adapter.fromJson("\"a\"")).isEqualTo('a'); assertThat(adapter.fromJson("'a'")).isEqualTo('a'); assertThat(adapter.toJson('b')).isEqualTo("\"b\""); try { // Only a single character is allowed. adapter.fromJson("'ab'"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a char but was \"ab\" at path $"); } // Allow nulls for Character.class assertThat(adapter.fromJson("null")).isEqualTo(null); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void doubleAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(double.class).lenient(); assertThat(adapter.fromJson("1.0")).isEqualTo(1.0); assertThat(adapter.fromJson("1")).isEqualTo(1.0); assertThat(adapter.fromJson("1e0")).isEqualTo(1.0); assertThat(adapter.toJson(-2.0)).isEqualTo("-2.0"); // Test min/max values. assertThat(adapter.fromJson("-1.7976931348623157E308")).isEqualTo(-Double.MAX_VALUE); assertThat(adapter.toJson(-Double.MAX_VALUE)).isEqualTo("-1.7976931348623157E308"); assertThat(adapter.fromJson("1.7976931348623157E308")).isEqualTo(Double.MAX_VALUE); assertThat(adapter.toJson(Double.MAX_VALUE)).isEqualTo("1.7976931348623157E308"); // Lenient reader converts too large values to infinities. assertThat(adapter.fromJson("1E309")).isEqualTo(Double.POSITIVE_INFINITY); assertThat(adapter.fromJson("-1E309")).isEqualTo(Double.NEGATIVE_INFINITY); assertThat(adapter.fromJson("+Infinity")).isEqualTo(Double.POSITIVE_INFINITY); assertThat(adapter.fromJson("Infinity")).isEqualTo(Double.POSITIVE_INFINITY); assertThat(adapter.fromJson("-Infinity")).isEqualTo(Double.NEGATIVE_INFINITY); // Nulls not allowed for double.class try { adapter.fromJson("null"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a double but was NULL at path $"); } try { adapter.toJson(null); fail(); } catch (NullPointerException expected) { } // Non-lenient adapter won't allow values outside of range. adapter = moshi.adapter(double.class); JsonReader reader = newReader("[1E309]"); reader.beginArray(); try { adapter.fromJson(reader); fail(); } catch (IOException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("JSON forbids NaN and infinities: Infinity at path $[0]"); } reader = newReader("[-1E309]"); reader.beginArray(); try { adapter.fromJson(reader); fail(); } catch (IOException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("JSON forbids NaN and infinities: -Infinity at path $[0]"); } } @Test public void DoubleAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Double.class).lenient(); assertThat(adapter.fromJson("1.0")).isEqualTo(1.0); assertThat(adapter.fromJson("1")).isEqualTo(1.0); assertThat(adapter.fromJson("1e0")).isEqualTo(1.0); assertThat(adapter.toJson(-2.0)).isEqualTo("-2.0"); // Allow nulls for Double.class assertThat(adapter.fromJson("null")).isEqualTo(null); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void floatAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(float.class).lenient(); assertThat(adapter.fromJson("1.0")).isEqualTo(1.0f); assertThat(adapter.fromJson("1")).isEqualTo(1.0f); assertThat(adapter.fromJson("1e0")).isEqualTo(1.0f); assertThat(adapter.toJson(-2.0f)).isEqualTo("-2.0"); // Test min/max values. assertThat(adapter.fromJson("-3.4028235E38")).isEqualTo(-Float.MAX_VALUE); assertThat(adapter.toJson(-Float.MAX_VALUE)).isEqualTo("-3.4028235E38"); assertThat(adapter.fromJson("3.4028235E38")).isEqualTo(Float.MAX_VALUE); assertThat(adapter.toJson(Float.MAX_VALUE)).isEqualTo("3.4028235E38"); // Lenient reader converts too large values to infinities. assertThat(adapter.fromJson("1E39")).isEqualTo(Float.POSITIVE_INFINITY); assertThat(adapter.fromJson("-1E39")).isEqualTo(Float.NEGATIVE_INFINITY); assertThat(adapter.fromJson("+Infinity")).isEqualTo(Float.POSITIVE_INFINITY); assertThat(adapter.fromJson("Infinity")).isEqualTo(Float.POSITIVE_INFINITY); assertThat(adapter.fromJson("-Infinity")).isEqualTo(Float.NEGATIVE_INFINITY); // Nulls not allowed for float.class try { adapter.fromJson("null"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a double but was NULL at path $"); } try { adapter.toJson(null); fail(); } catch (NullPointerException expected) { } // Non-lenient adapter won't allow values outside of range. adapter = moshi.adapter(float.class); JsonReader reader = newReader("[1E39]"); reader.beginArray(); try { adapter.fromJson(reader); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("JSON forbids NaN and infinities: Infinity at path $[1]"); } reader = newReader("[-1E39]"); reader.beginArray(); try { adapter.fromJson(reader); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("JSON forbids NaN and infinities: -Infinity at path $[1]"); } } @Test public void FloatAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Float.class).lenient(); assertThat(adapter.fromJson("1.0")).isEqualTo(1.0f); assertThat(adapter.fromJson("1")).isEqualTo(1.0f); assertThat(adapter.fromJson("1e0")).isEqualTo(1.0f); assertThat(adapter.toJson(-2.0f)).isEqualTo("-2.0"); // Allow nulls for Float.class assertThat(adapter.fromJson("null")).isEqualTo(null); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void intAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(int.class).lenient(); assertThat(adapter.fromJson("1")).isEqualTo(1); assertThat(adapter.toJson(-2)).isEqualTo("-2"); // Test min/max values assertThat(adapter.fromJson("-2147483648")).isEqualTo(Integer.MIN_VALUE); assertThat(adapter.toJson(Integer.MIN_VALUE)).isEqualTo("-2147483648"); assertThat(adapter.fromJson("2147483647")).isEqualTo(Integer.MAX_VALUE); assertThat(adapter.toJson(Integer.MAX_VALUE)).isEqualTo("2147483647"); try { adapter.fromJson("2147483648"); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected an int but was 2147483648 at path $"); } try { adapter.fromJson("-2147483649"); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected an int but was -2147483649 at path $"); } // Nulls not allowed for int.class try { adapter.fromJson("null"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected an int but was NULL at path $"); } try { adapter.toJson(null); fail(); } catch (NullPointerException expected) { } } @Test public void IntegerAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Integer.class).lenient(); assertThat(adapter.fromJson("1")).isEqualTo(1); assertThat(adapter.toJson(-2)).isEqualTo("-2"); // Allow nulls for Integer.class assertThat(adapter.fromJson("null")).isEqualTo(null); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void longAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(long.class).lenient(); assertThat(adapter.fromJson("1")).isEqualTo(1L); assertThat(adapter.toJson(-2L)).isEqualTo("-2"); // Test min/max values assertThat(adapter.fromJson("-9223372036854775808")).isEqualTo(Long.MIN_VALUE); assertThat(adapter.toJson(Long.MIN_VALUE)).isEqualTo("-9223372036854775808"); assertThat(adapter.fromJson("9223372036854775807")).isEqualTo(Long.MAX_VALUE); assertThat(adapter.toJson(Long.MAX_VALUE)).isEqualTo("9223372036854775807"); try { adapter.fromJson("9223372036854775808"); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected a long but was 9223372036854775808 at path $"); } try { adapter.fromJson("-9223372036854775809"); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected a long but was -9223372036854775809 at path $"); } // Nulls not allowed for long.class try { adapter.fromJson("null"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a long but was NULL at path $"); } try { adapter.toJson(null); fail(); } catch (NullPointerException expected) { } } @Test public void LongAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Long.class).lenient(); assertThat(adapter.fromJson("1")).isEqualTo(1L); assertThat(adapter.toJson(-2L)).isEqualTo("-2"); // Allow nulls for Integer.class assertThat(adapter.fromJson("null")).isEqualTo(null); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void shortAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(short.class).lenient(); assertThat(adapter.fromJson("1")).isEqualTo((short) 1); assertThat(adapter.toJson((short) -2)).isEqualTo("-2"); // Test min/max values. assertThat(adapter.fromJson("-32768")).isEqualTo(Short.MIN_VALUE); assertThat(adapter.toJson(Short.MIN_VALUE)).isEqualTo("-32768"); assertThat(adapter.fromJson("32767")).isEqualTo(Short.MAX_VALUE); assertThat(adapter.toJson(Short.MAX_VALUE)).isEqualTo("32767"); try { adapter.fromJson("32768"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a short but was 32768 at path $"); } try { adapter.fromJson("-32769"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected a short but was -32769 at path $"); } // Nulls not allowed for short.class try { adapter.fromJson("null"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Expected an int but was NULL at path $"); } try { adapter.toJson(null); fail(); } catch (NullPointerException expected) { } } @Test public void ShortAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Short.class).lenient(); assertThat(adapter.fromJson("1")).isEqualTo((short) 1); assertThat(adapter.toJson((short) -2)).isEqualTo("-2"); // Allow nulls for Byte.class assertThat(adapter.fromJson("null")).isEqualTo(null); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void stringAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(String.class).lenient(); assertThat(adapter.fromJson("\"a\"")).isEqualTo("a"); assertThat(adapter.toJson("b")).isEqualTo("\"b\""); assertThat(adapter.fromJson("null")).isEqualTo(null); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void upperBoundedWildcardsAreHandled() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Types.subtypeOf(String.class)); assertThat(adapter.fromJson("\"a\"")).isEqualTo("a"); assertThat(adapter.toJson("b")).isEqualTo("\"b\""); assertThat(adapter.fromJson("null")).isEqualTo(null); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void lowerBoundedWildcardsAreNotHandled() { Moshi moshi = new Moshi.Builder().build(); try { moshi.adapter(Types.supertypeOf(String.class)); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo("No JsonAdapter for ? super java.lang.String (with no annotations)"); } } @Test public void addNullFails() throws Exception { Type type = Object.class; Class annotation = Annotation.class; Moshi.Builder builder = new Moshi.Builder(); try { builder.add((null)); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add((Object) null); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add((KType) null, null); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add(type, null); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add(null, null, null); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add(type, null, null); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { builder.add(type, annotation, null); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } } @Test public void customJsonAdapter() throws Exception { Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build(); JsonAdapter jsonAdapter = moshi.adapter(Pizza.class); assertThat(jsonAdapter.toJson(new Pizza(15, true))) .isEqualTo("{\"size\":15,\"extra cheese\":true}"); assertThat(jsonAdapter.fromJson("{\"extra cheese\":true,\"size\":18}")) .isEqualTo(new Pizza(18, true)); } @Test public void classAdapterToObjectAndFromObject() throws Exception { Moshi moshi = new Moshi.Builder().build(); Pizza pizza = new Pizza(15, true); Map pizzaObject = new LinkedHashMap<>(); pizzaObject.put("diameter", 15L); pizzaObject.put("extraCheese", true); JsonAdapter jsonAdapter = moshi.adapter(Pizza.class); assertThat(jsonAdapter.toJsonValue(pizza)).isEqualTo(pizzaObject); assertThat(jsonAdapter.fromJsonValue(pizzaObject)).isEqualTo(pizza); } @Test public void customJsonAdapterToObjectAndFromObject() throws Exception { Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build(); Pizza pizza = new Pizza(15, true); Map pizzaObject = new LinkedHashMap<>(); pizzaObject.put("size", 15L); pizzaObject.put("extra cheese", true); JsonAdapter jsonAdapter = moshi.adapter(Pizza.class); assertThat(jsonAdapter.toJsonValue(pizza)).isEqualTo(pizzaObject); assertThat(jsonAdapter.fromJsonValue(pizzaObject)).isEqualTo(pizza); } @Test public void indent() throws Exception { Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build(); JsonAdapter jsonAdapter = moshi.adapter(Pizza.class); Pizza pizza = new Pizza(15, true); assertThat(jsonAdapter.indent(" ").toJson(pizza)) .isEqualTo("" + "{\n" + " \"size\": 15,\n" + " \"extra cheese\": true\n" + "}"); } @Test public void unindent() throws Exception { Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build(); JsonAdapter jsonAdapter = moshi.adapter(Pizza.class); Buffer buffer = new Buffer(); JsonWriter writer = JsonWriter.of(buffer); writer.setLenient(true); writer.setIndent(" "); Pizza pizza = new Pizza(15, true); // Calling JsonAdapter.indent("") can remove indentation. jsonAdapter.indent("").toJson(writer, pizza); assertThat(buffer.readUtf8()).isEqualTo("{\"size\":15,\"extra cheese\":true}"); // Indentation changes only apply to their use. jsonAdapter.toJson(writer, pizza); assertThat(buffer.readUtf8()) .isEqualTo("" + "{\n" + " \"size\": 15,\n" + " \"extra cheese\": true\n" + "}"); } @Test public void composingJsonAdapterFactory() throws Exception { Moshi moshi = new Moshi.Builder() .add(new MealDealAdapterFactory()) .add(Pizza.class, new PizzaAdapter()) .build(); JsonAdapter jsonAdapter = moshi.adapter(MealDeal.class); assertThat(jsonAdapter.toJson(new MealDeal(new Pizza(15, true), "Pepsi"))) .isEqualTo("[{\"size\":15,\"extra cheese\":true},\"Pepsi\"]"); assertThat(jsonAdapter.fromJson("[{\"extra cheese\":true,\"size\":18},\"Coke\"]")) .isEqualTo(new MealDeal(new Pizza(18, true), "Coke")); } static class Message { String speak; @Uppercase String shout; } @Test public void registerJsonAdapterForAnnotatedType() throws Exception { JsonAdapter uppercaseAdapter = new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { throw new AssertionError(); } @Override public void toJson(JsonWriter writer, String value) throws IOException { writer.value(value.toUpperCase(Locale.US)); } }; Moshi moshi = new Moshi.Builder().add(String.class, Uppercase.class, uppercaseAdapter).build(); JsonAdapter messageAdapter = moshi.adapter(Message.class); Message message = new Message(); message.speak = "Yo dog"; message.shout = "What's up"; assertThat(messageAdapter.toJson(message)) .isEqualTo("{\"shout\":\"WHAT'S UP\",\"speak\":\"Yo dog\"}"); } @Test public void adapterLookupDisallowsNullType() { Moshi moshi = new Moshi.Builder().build(); try { moshi.adapter(((Type) null), Collections.emptySet()); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } } @Test public void adapterLookupDisallowsNullAnnotations() { Moshi moshi = new Moshi.Builder().build(); try { moshi.adapter(String.class, (Class) null); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } try { moshi.adapter(String.class, (Set) null); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } } @Test public void nextJsonAdapterDisallowsNullAnnotations() throws Exception { JsonAdapter.Factory badFactory = new JsonAdapter.Factory() { @Nullable @Override public JsonAdapter create( Type type, Set annotations, Moshi moshi) { return moshi.nextAdapter(this, type, null); } }; Moshi moshi = new Moshi.Builder().add(badFactory).build(); try { moshi.adapter(Object.class); fail(); } catch (NullPointerException expected) { assertThat(expected).hasMessageThat().contains("Parameter specified as non-null is null"); } } @Uppercase static String uppercaseString; @Test public void delegatingJsonAdapterFactory() throws Exception { Moshi moshi = new Moshi.Builder().add(new UppercaseAdapterFactory()).build(); Field uppercaseString = MoshiTest.class.getDeclaredField("uppercaseString"); Set annotations = Util.getJsonAnnotations(uppercaseString); JsonAdapter adapter = moshi.adapter(String.class, annotations).lenient(); assertThat(adapter.toJson("a")).isEqualTo("\"A\""); assertThat(adapter.fromJson("\"b\"")).isEqualTo("B"); } @Test public void listJsonAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter> adapter = moshi.adapter(Types.newParameterizedType(List.class, String.class)); assertThat(adapter.toJson(Arrays.asList("a", "b"))).isEqualTo("[\"a\",\"b\"]"); assertThat(adapter.fromJson("[\"a\",\"b\"]")).isEqualTo(Arrays.asList("a", "b")); } @Test public void setJsonAdapter() throws Exception { Set set = new LinkedHashSet<>(); set.add("a"); set.add("b"); Moshi moshi = new Moshi.Builder().build(); JsonAdapter> adapter = moshi.adapter(Types.newParameterizedType(Set.class, String.class)); assertThat(adapter.toJson(set)).isEqualTo("[\"a\",\"b\"]"); assertThat(adapter.fromJson("[\"a\",\"b\"]")).isEqualTo(set); } @Test public void collectionJsonAdapter() throws Exception { Collection collection = new ArrayDeque<>(); collection.add("a"); collection.add("b"); Moshi moshi = new Moshi.Builder().build(); JsonAdapter> adapter = moshi.adapter(Types.newParameterizedType(Collection.class, String.class)); assertThat(adapter.toJson(collection)).isEqualTo("[\"a\",\"b\"]"); assertThat(adapter.fromJson("[\"a\",\"b\"]")).containsExactly("a", "b"); } @Uppercase static List uppercaseStrings; @Test public void collectionsDoNotKeepAnnotations() throws Exception { Moshi moshi = new Moshi.Builder().add(new UppercaseAdapterFactory()).build(); Field uppercaseStringsField = MoshiTest.class.getDeclaredField("uppercaseStrings"); try { moshi.adapter( uppercaseStringsField.getGenericType(), Util.getJsonAnnotations(uppercaseStringsField)); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "No JsonAdapter for java.util.List " + "annotated [@com.squareup.moshi.MoshiTest.Uppercase()]"); } } @Test public void noTypeAdapterForQualifiedPlatformType() throws Exception { Moshi moshi = new Moshi.Builder().build(); Field uppercaseStringField = MoshiTest.class.getDeclaredField("uppercaseString"); try { moshi.adapter( uppercaseStringField.getGenericType(), Util.getJsonAnnotations(uppercaseStringField)); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "No JsonAdapter for class java.lang.String " + "annotated [@com.squareup.moshi.MoshiTest.Uppercase()]"); } } @Test public void objectArray() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(String[].class); assertThat(adapter.toJson(new String[] {"a", "b"})).isEqualTo("[\"a\",\"b\"]"); assertThat(adapter.fromJson("[\"a\",\"b\"]")).asList().containsExactly("a", "b").inOrder(); } @Test public void primitiveArray() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(int[].class); assertThat(adapter.toJson(new int[] {1, 2})).isEqualTo("[1,2]"); assertThat(adapter.fromJson("[2,3]")).asList().containsExactly(2, 3).inOrder(); } @Test public void enumAdapter() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Roshambo.class).lenient(); assertThat(adapter.fromJson("\"ROCK\"")).isEqualTo(Roshambo.ROCK); assertThat(adapter.toJson(Roshambo.PAPER)).isEqualTo("\"PAPER\""); } @Test public void annotatedEnum() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Roshambo.class).lenient(); assertThat(adapter.fromJson("\"scr\"")).isEqualTo(Roshambo.SCISSORS); assertThat(adapter.toJson(Roshambo.SCISSORS)).isEqualTo("\"scr\""); } @Test public void invalidEnum() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Roshambo.class); try { adapter.fromJson("\"SPOCK\""); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected one of [ROCK, PAPER, scr] but was SPOCK at path $"); } } @Test public void invalidEnumHasCorrectPathInExceptionMessage() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Roshambo.class); JsonReader reader = JsonReader.of(new Buffer().writeUtf8("[\"SPOCK\"]")); reader.beginArray(); try { adapter.fromJson(reader); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected one of [ROCK, PAPER, scr] but was SPOCK at path $[0]"); } reader.endArray(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void nullEnum() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Roshambo.class).lenient(); assertThat(adapter.fromJson("null")).isNull(); assertThat(adapter.toJson(null)).isEqualTo("null"); } @Test public void byDefaultUnknownFieldsAreIgnored() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Pizza.class); Pizza pizza = adapter.fromJson("{\"diameter\":5,\"crust\":\"thick\",\"extraCheese\":true}"); assertThat(pizza.diameter).isEqualTo(5); assertThat(pizza.extraCheese).isEqualTo(true); } @Test public void failOnUnknownThrowsOnUnknownFields() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Pizza.class).failOnUnknown(); try { adapter.fromJson("{\"diameter\":5,\"crust\":\"thick\",\"extraCheese\":true}"); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Cannot skip unexpected NAME at $.crust"); } } @Test public void platformTypeThrows() throws IOException { Moshi moshi = new Moshi.Builder().build(); try { moshi.adapter(File.class); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo("Platform class java.io.File requires explicit JsonAdapter to be registered"); } try { moshi.adapter(KeyGenerator.class); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo( "Platform class javax.crypto.KeyGenerator requires explicit " + "JsonAdapter to be registered"); } try { moshi.adapter(Pair.class); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo( "Platform class android.util.Pair requires explicit JsonAdapter to be registered"); } } @Test public void collectionClassesHaveClearErrorMessage() { Moshi moshi = new Moshi.Builder().build(); try { moshi.adapter(Types.newParameterizedType(ArrayList.class, String.class)); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo( "No JsonAdapter for " + "java.util.ArrayList, " + "you should probably use List instead of ArrayList " + "(Moshi only supports the collection interfaces by default) " + "or else register a custom JsonAdapter."); } try { moshi.adapter(Types.newParameterizedType(HashMap.class, String.class, String.class)); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo( "No JsonAdapter for " + "java.util.HashMap, " + "you should probably use Map instead of HashMap " + "(Moshi only supports the collection interfaces by default) " + "or else register a custom JsonAdapter."); } } @Test public void noCollectionErrorIfAdapterExplicitlyProvided() { Moshi moshi = new Moshi.Builder() .add( new JsonAdapter.Factory() { @Override public JsonAdapter create( Type type, Set annotations, Moshi moshi) { return new MapJsonAdapter(moshi, String.class, String.class); } }) .build(); JsonAdapter> adapter = moshi.adapter(Types.newParameterizedType(HashMap.class, String.class, String.class)); assertThat(adapter).isInstanceOf(MapJsonAdapter.class); } static final class HasPlatformType { UUID uuid; static final class Wrapper { HasPlatformType hasPlatformType; } static final class ListWrapper { List platformTypes; } } @Test public void reentrantFieldErrorMessagesTopLevelMap() { Moshi moshi = new Moshi.Builder().build(); try { moshi.adapter(Types.newParameterizedType(Map.class, String.class, HasPlatformType.class)); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo( "Platform class java.util.UUID requires explicit " + "JsonAdapter to be registered" + "\nfor class java.util.UUID uuid" + "\nfor class com.squareup.moshi.MoshiTest$HasPlatformType" + "\nfor java.util.Map"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e.getCause()) .hasMessageThat() .isEqualTo( "Platform class java.util.UUID " + "requires explicit JsonAdapter to be registered"); } } @Test public void reentrantFieldErrorMessagesWrapper() { Moshi moshi = new Moshi.Builder().build(); try { moshi.adapter(HasPlatformType.Wrapper.class); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo( "Platform class java.util.UUID requires explicit " + "JsonAdapter to be registered" + "\nfor class java.util.UUID uuid" + "\nfor class com.squareup.moshi.MoshiTest$HasPlatformType hasPlatformType" + "\nfor class com.squareup.moshi.MoshiTest$HasPlatformType$Wrapper"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e.getCause()) .hasMessageThat() .isEqualTo( "Platform class java.util.UUID " + "requires explicit JsonAdapter to be registered"); } } @Test public void reentrantFieldErrorMessagesListWrapper() { Moshi moshi = new Moshi.Builder().build(); try { moshi.adapter(HasPlatformType.ListWrapper.class); fail(); } catch (IllegalArgumentException e) { assertThat(e) .hasMessageThat() .isEqualTo( "Platform class java.util.UUID requires explicit " + "JsonAdapter to be registered" + "\nfor class java.util.UUID uuid" + "\nfor class com.squareup.moshi.MoshiTest$HasPlatformType" + "\nfor java.util.List platformTypes" + "\nfor class com.squareup.moshi.MoshiTest$HasPlatformType$ListWrapper"); assertThat(e).hasCauseThat().isInstanceOf(IllegalArgumentException.class); assertThat(e.getCause()) .hasMessageThat() .isEqualTo( "Platform class java.util.UUID " + "requires explicit JsonAdapter to be registered"); } } @Test public void qualifierWithElementsMayNotBeDirectlyRegistered() throws IOException { try { new Moshi.Builder() .add( Boolean.class, Localized.class, StandardJsonAdapters.INSTANCE.getBOOLEAN_JSON_ADAPTER()); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Use JsonAdapter.Factory for annotations with elements"); } } @Test public void qualifierWithElements() throws IOException { Moshi moshi = new Moshi.Builder().add(LocalizedBooleanAdapter.FACTORY).build(); Baguette baguette = new Baguette(); baguette.avecBeurre = true; baguette.withButter = true; JsonAdapter adapter = moshi.adapter(Baguette.class); assertThat(adapter.toJson(baguette)) .isEqualTo("{\"avecBeurre\":\"oui\",\"withButter\":\"yes\"}"); Baguette decoded = adapter.fromJson("{\"avecBeurre\":\"oui\",\"withButter\":\"yes\"}"); assertThat(decoded.avecBeurre).isTrue(); assertThat(decoded.withButter).isTrue(); } /** Note that this is the opposite of Gson's behavior, where later adapters are preferred. */ @Test public void adaptersRegisteredInOrderOfPrecedence() throws Exception { JsonAdapter adapter1 = new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { throw new AssertionError(); } @Override public void toJson(JsonWriter writer, String value) throws IOException { writer.value("one!"); } }; JsonAdapter adapter2 = new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { throw new AssertionError(); } @Override public void toJson(JsonWriter writer, String value) throws IOException { writer.value("two!"); } }; Moshi moshi = new Moshi.Builder().add(String.class, adapter1).add(String.class, adapter2).build(); JsonAdapter adapter = moshi.adapter(String.class).lenient(); assertThat(adapter.toJson("a")).isEqualTo("\"one!\""); } @Test public void cachingJsonAdapters() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter1 = moshi.adapter(MealDeal.class); JsonAdapter adapter2 = moshi.adapter(MealDeal.class); assertThat(adapter1).isSameInstanceAs(adapter2); } @Test public void newBuilder() throws Exception { Moshi moshi = new Moshi.Builder().add(Pizza.class, new PizzaAdapter()).build(); Moshi.Builder newBuilder = moshi.newBuilder(); for (JsonAdapter.Factory factory : Moshi.BUILT_IN_FACTORIES) { // Awkward but java sources don't know about the internal-ness of this assertThat(factory).isNotIn(newBuilder.getFactories$moshi()); } } @Test public void referenceCyclesOnArrays() throws Exception { Moshi moshi = new Moshi.Builder().build(); Map map = new LinkedHashMap<>(); map.put("a", map); try { moshi.adapter(Object.class).toJson(map); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Nesting too deep at $" + repeat(".a", 255) + ": circular reference?"); } } @Test public void referenceCyclesOnObjects() throws Exception { Moshi moshi = new Moshi.Builder().build(); List list = new ArrayList<>(); list.add(list); try { moshi.adapter(Object.class).toJson(list); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Nesting too deep at $" + repeat("[0]", 255) + ": circular reference?"); } } @Test public void referenceCyclesOnMixedTypes() throws Exception { Moshi moshi = new Moshi.Builder().build(); List list = new ArrayList<>(); Map map = new LinkedHashMap<>(); list.add(map); map.put("a", list); try { moshi.adapter(Object.class).toJson(list); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Nesting too deep at $[0]" + repeat(".a[0]", 127) + ": circular reference?"); } } @Test public void duplicateKeyDisallowedInObjectType() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Object.class); String json = "{\"diameter\":5,\"diameter\":5,\"extraCheese\":true}"; try { adapter.fromJson(json); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Map key 'diameter' has multiple values at path $.diameter: 5.0 and 5.0"); } } @Test public void duplicateKeysAllowedInCustomType() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Pizza.class); String json = "{\"diameter\":5,\"diameter\":5,\"extraCheese\":true}"; assertThat(adapter.fromJson(json)).isEqualTo(new Pizza(5, true)); } @Test public void precedence() throws Exception { Moshi moshi = new Moshi.Builder() .add(new AppendingAdapterFactory(" a")) .addLast(new AppendingAdapterFactory(" y")) .add(new AppendingAdapterFactory(" b")) .addLast(new AppendingAdapterFactory(" z")) .build(); JsonAdapter adapter = moshi.adapter(String.class).lenient(); assertThat(adapter.toJson("hello")).isEqualTo("\"hello a b y z\""); } @Test public void precedenceWithNewBuilder() throws Exception { Moshi moshi1 = new Moshi.Builder() .add(new AppendingAdapterFactory(" a")) .addLast(new AppendingAdapterFactory(" w")) .add(new AppendingAdapterFactory(" b")) .addLast(new AppendingAdapterFactory(" x")) .build(); Moshi moshi2 = moshi1 .newBuilder() .add(new AppendingAdapterFactory(" c")) .addLast(new AppendingAdapterFactory(" y")) .add(new AppendingAdapterFactory(" d")) .addLast(new AppendingAdapterFactory(" z")) .build(); JsonAdapter adapter = moshi2.adapter(String.class).lenient(); assertThat(adapter.toJson("hello")).isEqualTo("\"hello a b c d w x y z\""); } /** Adds a suffix to a string before emitting it. */ static final class AppendingAdapterFactory implements JsonAdapter.Factory { private final String suffix; AppendingAdapterFactory(String suffix) { this.suffix = suffix; } @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { if (type != String.class) return null; final JsonAdapter delegate = moshi.nextAdapter(this, type, annotations); return new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { throw new AssertionError(); } @Override public void toJson(JsonWriter writer, String value) throws IOException { delegate.toJson(writer, value + suffix); } }; } } static class Pizza { final int diameter; final boolean extraCheese; Pizza(int diameter, boolean extraCheese) { this.diameter = diameter; this.extraCheese = extraCheese; } @Override public boolean equals(Object o) { return o instanceof Pizza && ((Pizza) o).diameter == diameter && ((Pizza) o).extraCheese == extraCheese; } @Override public int hashCode() { return diameter * (extraCheese ? 31 : 1); } } static class MealDeal { final Pizza pizza; final String drink; MealDeal(Pizza pizza, String drink) { this.pizza = pizza; this.drink = drink; } @Override public boolean equals(Object o) { return o instanceof MealDeal && ((MealDeal) o).pizza.equals(pizza) && ((MealDeal) o).drink.equals(drink); } @Override public int hashCode() { return pizza.hashCode() + (31 * drink.hashCode()); } } static class PizzaAdapter extends JsonAdapter { @Override public Pizza fromJson(JsonReader reader) throws IOException { int diameter = 13; boolean extraCheese = false; reader.beginObject(); while (reader.hasNext()) { String name = reader.nextName(); if (name.equals("size")) { diameter = reader.nextInt(); } else if (name.equals("extra cheese")) { extraCheese = reader.nextBoolean(); } else { reader.skipValue(); } } reader.endObject(); return new Pizza(diameter, extraCheese); } @Override public void toJson(JsonWriter writer, Pizza value) throws IOException { writer.beginObject(); writer.name("size").value(value.diameter); writer.name("extra cheese").value(value.extraCheese); writer.endObject(); } } static class MealDealAdapterFactory implements JsonAdapter.Factory { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { if (!type.equals(MealDeal.class)) return null; final JsonAdapter pizzaAdapter = moshi.adapter(Pizza.class); final JsonAdapter drinkAdapter = moshi.adapter(String.class); return new JsonAdapter() { @Override public MealDeal fromJson(JsonReader reader) throws IOException { reader.beginArray(); Pizza pizza = pizzaAdapter.fromJson(reader); String drink = drinkAdapter.fromJson(reader); reader.endArray(); return new MealDeal(pizza, drink); } @Override public void toJson(JsonWriter writer, MealDeal value) throws IOException { writer.beginArray(); pizzaAdapter.toJson(writer, value.pizza); drinkAdapter.toJson(writer, value.drink); writer.endArray(); } }; } } @Retention(RUNTIME) @JsonQualifier public @interface Uppercase {} static class UppercaseAdapterFactory implements JsonAdapter.Factory { @Override public JsonAdapter create(Type type, Set annotations, Moshi moshi) { if (!type.equals(String.class)) return null; if (!Util.isAnnotationPresent(annotations, Uppercase.class)) return null; final JsonAdapter stringAdapter = moshi.nextAdapter(this, String.class, Util.NO_ANNOTATIONS); return new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { String s = stringAdapter.fromJson(reader); return s.toUpperCase(Locale.US); } @Override public void toJson(JsonWriter writer, String value) throws IOException { stringAdapter.toJson(writer, value.toUpperCase()); } }; } } enum Roshambo { ROCK, PAPER, @Json(name = "scr") SCISSORS } @Retention(RUNTIME) @JsonQualifier @interface Localized { String value(); } static class Baguette { @Localized("en") boolean withButter; @Localized("fr") boolean avecBeurre; } static class LocalizedBooleanAdapter extends JsonAdapter { private static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() { @Override public JsonAdapter create( Type type, Set annotations, Moshi moshi) { if (type == boolean.class) { for (Annotation annotation : annotations) { if (annotation instanceof Localized) { return new LocalizedBooleanAdapter(((Localized) annotation).value()); } } } return null; } }; private final String trueString; private final String falseString; public LocalizedBooleanAdapter(String language) { if (language.equals("fr")) { trueString = "oui"; falseString = "non"; } else { trueString = "yes"; falseString = "no"; } } @Override public Boolean fromJson(JsonReader reader) throws IOException { return reader.nextString().equals(trueString); } @Override public void toJson(JsonWriter writer, Boolean value) throws IOException { writer.value(value ? trueString : falseString); } } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/MoshiTesting.kt ================================================ /* * Copyright (C) 2025 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 * * https://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:JvmName("MoshiTesting") package com.squareup.moshi fun jsonValueReader(value: Any?): JsonReader { return `-JsonValueReader`(value) } fun jsonValueWriter(): JsonWriter { return `-JsonValueWriter`() } fun root(writer: JsonWriter): Any? { return (writer as `-JsonValueWriter`).root() } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/ObjectAdapterTest.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.math.BigDecimal; import java.util.AbstractCollection; import java.util.AbstractList; import java.util.AbstractMap; import java.util.AbstractSet; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import javax.annotation.Nullable; import org.junit.Test; public final class ObjectAdapterTest { @Test public void toJsonUsesRuntimeType() { Delivery delivery = new Delivery(); delivery.address = "1455 Market St."; Pizza pizza = new Pizza(); pizza.diameter = 12; pizza.extraCheese = true; delivery.items = Arrays.asList(pizza, "Pepsi"); Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Object.class); assertThat(adapter.toJson(delivery)) .isEqualTo( "{" + "\"address\":\"1455 Market St.\"," + "\"items\":[" + "{\"diameter\":12,\"extraCheese\":true}," + "\"Pepsi\"" + "]" + "}"); } @Test public void toJsonJavaLangObject() { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Object.class); assertThat(adapter.toJson(new Object())).isEqualTo("{}"); } @Test public void fromJsonReturnsMapsAndLists() throws Exception { Map delivery = new LinkedHashMap<>(); delivery.put("address", "1455 Market St."); Map pizza = new LinkedHashMap<>(); pizza.put("diameter", 12d); pizza.put("extraCheese", true); delivery.put("items", Arrays.asList(pizza, "Pepsi")); Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Object.class); assertThat( adapter.fromJson( "{" + "\"address\":\"1455 Market St.\"," + "\"items\":[" + "{\"diameter\":12,\"extraCheese\":true}," + "\"Pepsi\"" + "]" + "}")) .isEqualTo(delivery); } @Test public void fromJsonUsesDoublesForNumbers() throws Exception { Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Object.class); assertThat(adapter.fromJson("[0, 1]")).isEqualTo(Arrays.asList(0d, 1d)); } @Test public void fromJsonDoesNotFailOnNullValues() throws Exception { Map emptyDelivery = new LinkedHashMap<>(); emptyDelivery.put("address", null); emptyDelivery.put("items", null); Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Object.class); assertThat(adapter.fromJson("{\"address\":null, \"items\":null}")).isEqualTo(emptyDelivery); } @Test public void toJsonCoercesRuntimeTypeForCollections() { Collection collection = new AbstractCollection() { @Override public Iterator iterator() { return Collections.singleton("A").iterator(); } @Override public int size() { return 1; } }; Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Object.class); assertThat(adapter.toJson(collection)).isEqualTo("[\"A\"]"); } @Test public void toJsonCoercesRuntimeTypeForLists() { List list = new AbstractList() { @Override public String get(int i) { return "A"; } @Override public int size() { return 1; } }; Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Object.class); assertThat(adapter.toJson(list)).isEqualTo("[\"A\"]"); } @Test public void toJsonCoercesRuntimeTypeForSets() { Set set = new AbstractSet() { @Override public Iterator iterator() { return Collections.singleton("A").iterator(); } @Override public int size() { return 1; } }; Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Object.class); assertThat(adapter.toJson(set)).isEqualTo("[\"A\"]"); } @Test public void toJsonCoercesRuntimeTypeForMaps() { Map map = new AbstractMap() { @Override public Set> entrySet() { return Collections.singletonMap("A", true).entrySet(); } }; Moshi moshi = new Moshi.Builder().build(); JsonAdapter adapter = moshi.adapter(Object.class); assertThat(adapter.toJson(map)).isEqualTo("{\"A\":true}"); } @Test public void toJsonUsesTypeAdapters() { Object dateAdapter = new Object() { @ToJson Long dateToJson(Date d) { return d.getTime(); } @FromJson Date dateFromJson(Long millis) { return new Date(millis); } }; Moshi moshi = new Moshi.Builder().add(dateAdapter).build(); JsonAdapter adapter = moshi.adapter(Object.class); assertThat(adapter.toJson(Arrays.asList(new Date(1), new Date(2)))).isEqualTo("[1,2]"); } /** * Confirm that the built-in adapter for Object delegates to user-supplied adapters for JSON value * types like strings. */ @Test public void objectAdapterDelegatesStringNamesAndValues() throws Exception { JsonAdapter stringAdapter = new JsonAdapter() { @Override public String fromJson(JsonReader reader) throws IOException { return reader.nextString().toUpperCase(Locale.US); } @Override public void toJson(JsonWriter writer, @Nullable String value) { throw new UnsupportedOperationException(); } }; Moshi moshi = new Moshi.Builder().add(String.class, stringAdapter).build(); JsonAdapter objectAdapter = moshi.adapter(Object.class); Map value = (Map) objectAdapter.fromJson("{\"a\":\"b\", \"c\":\"d\"}"); assertThat(value).containsExactly("A", "B", "C", "D"); } /** * Confirm that the built-in adapter for Object delegates to any user-supplied adapters for * Object. This is necessary to customize adapters for primitives like numbers. */ @Test public void objectAdapterDelegatesObjects() throws Exception { JsonAdapter.Factory objectFactory = new JsonAdapter.Factory() { @Override public @Nullable JsonAdapter create( Type type, Set annotations, Moshi moshi) { if (type != Object.class) return null; final JsonAdapter delegate = moshi.nextAdapter(this, Object.class, annotations); return new JsonAdapter() { @Override public @Nullable Object fromJson(JsonReader reader) throws IOException { if (reader.peek() != JsonReader.Token.NUMBER) { return delegate.fromJson(reader); } else { return new BigDecimal(reader.nextString()); } } @Override public void toJson(JsonWriter writer, @Nullable Object value) { throw new UnsupportedOperationException(); } }; } }; Moshi moshi = new Moshi.Builder().add(objectFactory).build(); JsonAdapter objectAdapter = moshi.adapter(Object.class); List value = (List) objectAdapter.fromJson("[0, 1, 2.0, 3.14]"); assertThat(value) .isEqualTo( Arrays.asList( new BigDecimal("0"), new BigDecimal("1"), new BigDecimal("2.0"), new BigDecimal("3.14"))); } /** Confirm that the built-in adapter for Object delegates to user-supplied adapters for lists. */ @Test public void objectAdapterDelegatesLists() throws Exception { JsonAdapter> listAdapter = new JsonAdapter>() { @Override public List fromJson(JsonReader reader) throws IOException { reader.skipValue(); return singletonList("z"); } @Override public void toJson(JsonWriter writer, @Nullable List value) { throw new UnsupportedOperationException(); } }; Moshi moshi = new Moshi.Builder().add(List.class, listAdapter).build(); JsonAdapter objectAdapter = moshi.adapter(Object.class); Map mapOfList = (Map) objectAdapter.fromJson("{\"a\":[\"b\"]}"); assertThat(mapOfList).isEqualTo(singletonMap("a", singletonList("z"))); } /** Confirm that the built-in adapter for Object delegates to user-supplied adapters for maps. */ @Test public void objectAdapterDelegatesMaps() throws Exception { JsonAdapter> mapAdapter = new JsonAdapter>() { @Override public Map fromJson(JsonReader reader) throws IOException { reader.skipValue(); return singletonMap("x", "y"); } @Override public void toJson(JsonWriter writer, @Nullable Map value) { throw new UnsupportedOperationException(); } }; Moshi moshi = new Moshi.Builder().add(Map.class, mapAdapter).build(); JsonAdapter objectAdapter = moshi.adapter(Object.class); List listOfMap = (List) objectAdapter.fromJson("[{\"b\":\"c\"}]"); assertThat(listOfMap).isEqualTo(singletonList(singletonMap("x", "y"))); } static class Delivery { String address; List items; } static class Pizza { int diameter; boolean extraCheese; } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/PromoteNameToValueTest.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import java.util.List; import okio.Buffer; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; @RunWith(Parameterized.class) public final class PromoteNameToValueTest { @Parameter public JsonCodecFactory factory; @Parameters(name = "{0}") public static List parameters() { return JsonCodecFactory.factories(); } @Test public void readerStringValue() throws Exception { JsonReader reader = factory.newReader("{\"a\":1}"); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.getPath()).isEqualTo("$.a"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextString()).isEqualTo("a"); assertThat(reader.getPath()).isEqualTo("$.a"); assertThat(reader.nextInt()).isEqualTo(1); assertThat(reader.getPath()).isEqualTo("$.a"); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void readerIntegerValue() throws Exception { JsonReader reader = factory.newReader("{\"5\":1}"); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.getPath()).isEqualTo("$.5"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextInt()).isEqualTo(5); assertThat(reader.getPath()).isEqualTo("$.5"); assertThat(reader.nextInt()).isEqualTo(1); assertThat(reader.getPath()).isEqualTo("$.5"); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void readerDoubleValue() throws Exception { JsonReader reader = factory.newReader("{\"5.5\":1}"); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.getPath()).isEqualTo("$.5.5"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextDouble()).isEqualTo(5.5d); assertThat(reader.getPath()).isEqualTo("$.5.5"); assertThat(reader.nextInt()).isEqualTo(1); assertThat(reader.getPath()).isEqualTo("$.5.5"); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void readerBooleanValue() throws Exception { JsonReader reader = factory.newReader("{\"true\":1}"); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.getPath()).isEqualTo("$.true"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); try { reader.nextBoolean(); fail(); } catch (JsonDataException e) { assertThat(e) .hasMessageThat() .isAnyOf( "Expected BOOLEAN but was true, a java.lang.String, at path $.true", "Expected a boolean but was STRING at path $.true"); } assertThat(reader.getPath()).isEqualTo("$.true"); assertThat(reader.nextString()).isEqualTo("true"); assertThat(reader.getPath()).isEqualTo("$.true"); assertThat(reader.nextInt()).isEqualTo(1); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void readerLongValue() throws Exception { JsonReader reader = factory.newReader("{\"5\":1}"); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.getPath()).isEqualTo("$.5"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextLong()).isEqualTo(5L); assertThat(reader.getPath()).isEqualTo("$.5"); assertThat(reader.nextInt()).isEqualTo(1); assertThat(reader.getPath()).isEqualTo("$.5"); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void readerNullValue() throws Exception { JsonReader reader = factory.newReader("{\"null\":1}"); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.getPath()).isEqualTo("$.null"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); try { reader.nextNull(); fail(); } catch (JsonDataException e) { assertThat(e) .hasMessageThat() .isAnyOf( "Expected NULL but was null, a java.lang.String, at path $.null", "Expected null but was STRING at path $.null"); } assertThat(reader.nextString()).isEqualTo("null"); assertThat(reader.getPath()).isEqualTo("$.null"); assertThat(reader.nextInt()).isEqualTo(1); assertThat(reader.getPath()).isEqualTo("$.null"); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void readerMultipleValueObject() throws Exception { JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}"); reader.beginObject(); assertThat(reader.nextName()).isEqualTo("a"); assertThat(reader.nextInt()).isEqualTo(1); reader.promoteNameToValue(); assertThat(reader.getPath()).isEqualTo("$.b"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.STRING); assertThat(reader.nextString()).isEqualTo("b"); assertThat(reader.getPath()).isEqualTo("$.b"); assertThat(reader.nextInt()).isEqualTo(2); assertThat(reader.getPath()).isEqualTo("$.b"); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void readerEmptyValueObject() throws Exception { JsonReader reader = factory.newReader("{}"); reader.beginObject(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_OBJECT); reader.promoteNameToValue(); assertThat(reader.getPath()).isEqualTo("$."); reader.endObject(); assertThat(reader.getPath()).isEqualTo("$"); } @Test public void readerUnusedPromotionDoesntPersist() throws Exception { JsonReader reader = factory.newReader("[{},{\"a\":5}]"); reader.beginArray(); reader.beginObject(); reader.promoteNameToValue(); reader.endObject(); reader.beginObject(); try { reader.nextString(); fail(); } catch (JsonDataException expected) { } assertThat(reader.nextName()).isEqualTo("a"); } @Test public void readerUnquotedIntegerValue() throws Exception { JsonReader reader = factory.newReader("{5:1}"); reader.setLenient(true); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.nextInt()).isEqualTo(5); assertThat(reader.nextInt()).isEqualTo(1); reader.endObject(); } @Test public void readerUnquotedLongValue() throws Exception { JsonReader reader = factory.newReader("{5:1}"); reader.setLenient(true); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.nextLong()).isEqualTo(5L); assertThat(reader.nextInt()).isEqualTo(1); reader.endObject(); } @Test public void readerUnquotedDoubleValue() throws Exception { JsonReader reader = factory.newReader("{5:1}"); reader.setLenient(true); reader.beginObject(); reader.promoteNameToValue(); assertThat(reader.nextDouble()).isEqualTo(5d); assertThat(reader.nextInt()).isEqualTo(1); reader.endObject(); } @Test public void writerStringValue() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); writer.value("a"); assertThat(writer.getPath()).isEqualTo("$.a"); writer.value(1); assertThat(writer.getPath()).isEqualTo("$.a"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); assertThat(factory.json()).isEqualTo("{\"a\":1}"); } @Test public void writerIntegerValue() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); writer.value(5); assertThat(writer.getPath()).isEqualTo("$.5"); writer.value(1); assertThat(writer.getPath()).isEqualTo("$.5"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); assertThat(factory.json()).isEqualTo("{\"5\":1}"); } @Test public void writerDoubleValue() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); writer.value(5.5d); assertThat(writer.getPath()).isEqualTo("$.5.5"); writer.value(1); assertThat(writer.getPath()).isEqualTo("$.5.5"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); assertThat(factory.json()).isEqualTo("{\"5.5\":1}"); } @Test public void writerBooleanValue() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); try { writer.value(true); fail(); } catch (IllegalStateException e) { assertThat(e) .hasMessageThat() .isEqualTo("Boolean cannot be used as a map key in JSON at path $."); } writer.value("true"); assertThat(writer.getPath()).isEqualTo("$.true"); writer.value(1); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); assertThat(factory.json()).isEqualTo("{\"true\":1}"); } @Test public void writerLongValue() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); writer.value(5L); assertThat(writer.getPath()).isEqualTo("$.5"); writer.value(1); assertThat(writer.getPath()).isEqualTo("$.5"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); assertThat(factory.json()).isEqualTo("{\"5\":1}"); } @Test public void writerNullValue() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); try { writer.nullValue(); fail(); } catch (IllegalStateException e) { assertThat(e) .hasMessageThat() .isEqualTo("null cannot be used as a map key in JSON at path $."); } writer.value("null"); assertThat(writer.getPath()).isEqualTo("$.null"); writer.value(1); assertThat(writer.getPath()).isEqualTo("$.null"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); assertThat(factory.json()).isEqualTo("{\"null\":1}"); } @Test public void writerMultipleValueObject() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.name("a"); writer.value(1); writer.promoteValueToName(); writer.value("b"); assertThat(writer.getPath()).isEqualTo("$.b"); writer.value(2); assertThat(writer.getPath()).isEqualTo("$.b"); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); assertThat(factory.json()).isEqualTo("{\"a\":1,\"b\":2}"); } @Test public void writerEmptyValueObject() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); assertThat(writer.getPath()).isEqualTo("$."); writer.endObject(); assertThat(writer.getPath()).isEqualTo("$"); assertThat(factory.json()).isEqualTo("{}"); } @Test public void writerUnusedPromotionDoesntPersist() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginArray(); writer.beginObject(); writer.promoteValueToName(); writer.endObject(); writer.beginObject(); try { writer.value("a"); fail(); } catch (IllegalStateException expected) { } writer.name("a"); } @Test public void writerSourceValueFails() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); try { writer.value(new Buffer().writeUtf8("\"a\"")); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("BufferedSource cannot be used as a map key in JSON at path $."); } writer.value("a"); writer.value("a value"); writer.endObject(); assertThat(factory.json()).isEqualTo("{\"a\":\"a value\"}"); } @Test public void writerValueSinkFails() throws Exception { JsonWriter writer = factory.newWriter(); writer.beginObject(); writer.promoteValueToName(); try { writer.valueSink(); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("BufferedSink cannot be used as a map key in JSON at path $."); } writer.value("a"); writer.value("a value"); writer.endObject(); assertThat(factory.json()).isEqualTo("{\"a\":\"a value\"}"); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/RecursiveTypesResolveTest.java ================================================ /* * Copyright (C) 2017 Gson Authors * * 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.squareup.moshi; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import com.squareup.moshi.internal.Util; import org.junit.Test; /** * Test fixes for infinite recursion on {@link Util#resolve(java.lang.reflect.Type, * java.lang.reflect.Type, Class)}, described at Issue #440 and similar issues. * *

These tests originally caused {@link StackOverflowError} because of infinite recursion on * attempts to resolve generics on types, with an intermediate types like 'Foo2<? extends ? super * ? extends ... ? extends A>' * *

Adapted from https://github.com/google/gson/commit/a300148003e3a067875b1444e8268b6e0f0e0e02 in * service of https://github.com/square/moshi/issues/338. */ public final class RecursiveTypesResolveTest { private static class Foo1 { public Foo2 foo2; } private static class Foo2 { public Foo1 foo1; } /** Test simplest case of recursion. */ @Test public void recursiveResolveSimple() { JsonAdapter adapter = new Moshi.Builder().build().adapter(Foo1.class); assertNotNull(adapter); } // // Tests belows check the behaviour of the methods changed for the fix // @Test public void doubleSupertype() { assertEquals( Types.supertypeOf(Number.class), Types.supertypeOf(Types.supertypeOf(Number.class))); } @Test public void doubleSubtype() { assertEquals(Types.subtypeOf(Number.class), Types.subtypeOf(Types.subtypeOf(Number.class))); } @Test public void superSubtype() { assertEquals(Types.subtypeOf(Object.class), Types.supertypeOf(Types.subtypeOf(Number.class))); } @Test public void subSupertype() { assertEquals(Types.subtypeOf(Object.class), Types.subtypeOf(Types.supertypeOf(Number.class))); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/TestUtil.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi; import java.util.Arrays; import okio.Buffer; public final class TestUtil { static final int MAX_DEPTH = 255; public static JsonReader newReader(String json) { Buffer buffer = new Buffer().writeUtf8(json); return JsonReader.of(buffer); } public static String repeat(char c, int count) { char[] array = new char[count]; Arrays.fill(array, c); return new String(array); } public static String repeat(String s, int count) { StringBuilder result = new StringBuilder(s.length() * count); for (int i = 0; i < count; i++) { result.append(s); } return result.toString(); } private TestUtil() { throw new AssertionError("No instances."); } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/TypesTest.java ================================================ /* * Copyright (C) 2010 Google 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.squareup.moshi; import static com.google.common.truth.Truth.assertThat; import static com.squareup.moshi.internal.Util.canonicalize; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.Assert.fail; import com.squareup.moshi.internal.Util; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import org.junit.Test; public final class TypesTest { @Retention(RUNTIME) @JsonQualifier @interface TestQualifier {} @Retention(RUNTIME) @JsonQualifier @interface AnotherTestQualifier {} @Retention(RUNTIME) @interface TestAnnotation {} @TestQualifier private Object hasTestQualifier; @Test public void nextAnnotationsRequiresJsonAnnotation() throws Exception { try { Types.nextAnnotations(Collections.emptySet(), TestAnnotation.class); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "interface com.squareup.moshi.TypesTest$TestAnnotation is not a JsonQualifier."); } } @Test public void nextAnnotationsDoesNotContainReturnsNull() throws Exception { Set annotations = Collections.singleton(Util.createJsonQualifierImplementation(AnotherTestQualifier.class)); assertThat(Types.nextAnnotations(annotations, TestQualifier.class)).isNull(); assertThat(Types.nextAnnotations(Collections.emptySet(), TestQualifier.class)) .isNull(); } @Test public void nextAnnotationsReturnsDelegateAnnotations() throws Exception { Set annotations = new LinkedHashSet<>(2); annotations.add(Util.createJsonQualifierImplementation(TestQualifier.class)); annotations.add(Util.createJsonQualifierImplementation(AnotherTestQualifier.class)); Set expected = Collections.singleton(Util.createJsonQualifierImplementation(AnotherTestQualifier.class)); assertThat(Types.nextAnnotations(Collections.unmodifiableSet(annotations), TestQualifier.class)) .isEqualTo(expected); } @Test public void newParameterizedType() throws Exception { // List. List is a top-level class. Type type = Types.newParameterizedType(List.class, A.class); assertThat(getFirstTypeArgument(type)).isEqualTo(A.class); // A. A is a static inner class. type = Types.newParameterizedTypeWithOwner(TypesTest.class, A.class, B.class); assertThat(getFirstTypeArgument(type)).isEqualTo(B.class); } @Test public void newParameterizedType_missingTypeVars() { try { Types.newParameterizedType(List.class); fail("Should have errored due to missing type variable"); } catch (Exception e) { assertThat(e).hasMessageThat().contains("Missing type arguments"); } try { Types.newParameterizedTypeWithOwner(TypesTest.class, A.class); fail("Should have errored due to missing type variable"); } catch (Exception e) { assertThat(e).hasMessageThat().contains("Missing type arguments"); } } @Test public void parameterizedTypeWithRequiredOwnerMissing() throws Exception { try { Types.newParameterizedType(A.class, B.class); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("unexpected owner type for " + A.class + ": null"); } } @Test public void parameterizedTypeWithUnnecessaryOwnerProvided() throws Exception { try { Types.newParameterizedTypeWithOwner(A.class, List.class, B.class); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("unexpected owner type for " + List.class + ": " + A.class); } } @Test public void parameterizedTypeWithIncorrectOwnerProvided() throws Exception { try { Types.newParameterizedTypeWithOwner(A.class, D.class, B.class); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("unexpected owner type for " + D.class + ": " + A.class); } } @Test public void arrayOf() { assertThat(Types.getRawType(Types.arrayOf(int.class))).isEqualTo(int[].class); assertThat(Types.getRawType(Types.arrayOf(List.class))).isEqualTo(List[].class); assertThat(Types.getRawType(Types.arrayOf(String[].class))).isEqualTo(String[][].class); } List listSubtype; List listSupertype; @Test public void subtypeOf() throws Exception { Type listOfWildcardType = TypesTest.class.getDeclaredField("listSubtype").getGenericType(); Type expected = Types.collectionElementType(listOfWildcardType, List.class); assertThat(Types.subtypeOf(CharSequence.class)).isEqualTo(expected); } @Test public void supertypeOf() throws Exception { Type listOfWildcardType = TypesTest.class.getDeclaredField("listSupertype").getGenericType(); Type expected = Types.collectionElementType(listOfWildcardType, List.class); assertThat(Types.supertypeOf(String.class)).isEqualTo(expected); } @Test public void getFirstTypeArgument() throws Exception { assertThat(getFirstTypeArgument(A.class)).isNull(); Type type = Types.newParameterizedTypeWithOwner(TypesTest.class, A.class, B.class, C.class); assertThat(getFirstTypeArgument(type)).isEqualTo(B.class); } @Test public void newParameterizedTypeObjectMethods() throws Exception { Type mapOfStringIntegerType = TypesTest.class.getDeclaredField("mapOfStringInteger").getGenericType(); ParameterizedType newMapType = Types.newParameterizedType(Map.class, String.class, Integer.class); assertThat(newMapType).isEqualTo(mapOfStringIntegerType); assertThat(newMapType.hashCode()).isEqualTo(mapOfStringIntegerType.hashCode()); assertThat(newMapType.toString()).isEqualTo(mapOfStringIntegerType.toString()); Type arrayListOfMapOfStringIntegerType = TypesTest.class.getDeclaredField("arrayListOfMapOfStringInteger").getGenericType(); ParameterizedType newListType = Types.newParameterizedType(ArrayList.class, newMapType); assertThat(newListType).isEqualTo(arrayListOfMapOfStringIntegerType); assertThat(newListType.hashCode()).isEqualTo(arrayListOfMapOfStringIntegerType.hashCode()); assertThat(newListType.toString()).isEqualTo(arrayListOfMapOfStringIntegerType.toString()); } private static final class A {} private static final class B {} private static final class C {} private static final class D {} private static final class E {} /** * Given a parameterized type {@code A}, returns B. If the specified type is not a generic * type, returns null. */ public static Type getFirstTypeArgument(Type type) throws Exception { if (!(type instanceof ParameterizedType)) return null; ParameterizedType ptype = (ParameterizedType) type; Type[] actualTypeArguments = ptype.getActualTypeArguments(); if (actualTypeArguments.length == 0) return null; return canonicalize(actualTypeArguments[0]); } Map mapOfStringInteger; Map[] arrayOfMapOfStringInteger; ArrayList> arrayListOfMapOfStringInteger; interface StringIntegerMap extends Map {} @Test public void arrayComponentType() throws Exception { assertThat(Util.arrayComponentType(String[][].class)).isEqualTo(String[].class); assertThat(Util.arrayComponentType(String[].class)).isEqualTo(String.class); Type arrayOfMapOfStringIntegerType = TypesTest.class.getDeclaredField("arrayOfMapOfStringInteger").getGenericType(); Type mapOfStringIntegerType = TypesTest.class.getDeclaredField("mapOfStringInteger").getGenericType(); assertThat(Util.arrayComponentType(arrayOfMapOfStringIntegerType)) .isEqualTo(mapOfStringIntegerType); } @Test public void collectionElementType() throws Exception { Type arrayListOfMapOfStringIntegerType = TypesTest.class.getDeclaredField("arrayListOfMapOfStringInteger").getGenericType(); Type mapOfStringIntegerType = TypesTest.class.getDeclaredField("mapOfStringInteger").getGenericType(); assertThat(Types.collectionElementType(arrayListOfMapOfStringIntegerType, List.class)) .isEqualTo(mapOfStringIntegerType); } @Test public void mapKeyAndValueTypes() throws Exception { Type mapOfStringIntegerType = TypesTest.class.getDeclaredField("mapOfStringInteger").getGenericType(); assertThat(Util.mapKeyAndValueTypes(mapOfStringIntegerType, Map.class)) .asList() .containsExactly(String.class, Integer.class) .inOrder(); } @Test public void propertiesTypes() throws Exception { assertThat(Util.mapKeyAndValueTypes(Properties.class, Properties.class)) .asList() .containsExactly(String.class, String.class) .inOrder(); } @Test public void fixedVariablesTypes() throws Exception { assertThat(Util.mapKeyAndValueTypes(StringIntegerMap.class, StringIntegerMap.class)) .asList() .containsExactly(String.class, Integer.class) .inOrder(); } @SuppressWarnings("GetClassOnAnnotation") // Explicitly checking for proxy implementation. @Test public void createJsonQualifierImplementation() throws Exception { TestQualifier actual = Util.createJsonQualifierImplementation(TestQualifier.class); TestQualifier expected = (TestQualifier) TypesTest.class.getDeclaredField("hasTestQualifier").getAnnotations()[0]; assertThat(actual.annotationType()).isEqualTo(TestQualifier.class); assertThat(actual).isEqualTo(expected); assertThat(actual).isNotEqualTo(null); assertThat(actual.hashCode()).isEqualTo(expected.hashCode()); assertThat(actual.getClass()).isNotEqualTo(TestQualifier.class); } @Test public void arrayEqualsGenericTypeArray() { assertThat(Types.equals(int[].class, Types.arrayOf(int.class))).isTrue(); assertThat(Types.equals(Types.arrayOf(int.class), int[].class)).isTrue(); assertThat(Types.equals(String[].class, Types.arrayOf(String.class))).isTrue(); assertThat(Types.equals(Types.arrayOf(String.class), String[].class)).isTrue(); } @Test public void parameterizedTypeMatchesClassWithGenericInfoFromReturn() { Type parameterizedEAB = Types.newParameterizedTypeWithOwner(TypesTest.class, E.class, A.class, B.class); Class eClass = E.class; assertThat(Types.equals(parameterizedEAB, eClass)).isTrue(); assertThat(Types.equals(eClass, parameterizedEAB)).isTrue(); Type parameterizedEBA = Types.newParameterizedTypeWithOwner(TypesTest.class, E.class, B.class, A.class); assertThat(Types.equals(parameterizedEBA, eClass)).isFalse(); assertThat(Types.equals(eClass, parameterizedEBA)).isFalse(); Type parameterizedEObjectObject = Types.newParameterizedTypeWithOwner(TypesTest.class, E.class, Object.class, Object.class); assertThat(Types.equals(parameterizedEObjectObject, eClass)).isFalse(); assertThat(Types.equals(eClass, parameterizedEObjectObject)).isFalse(); } @Test public void parameterizedAndWildcardTypesCannotHavePrimitiveArguments() throws Exception { try { Types.newParameterizedType(List.class, int.class); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Unexpected primitive int. Use the boxed type."); } try { Types.subtypeOf(byte.class); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Unexpected primitive byte. Use the boxed type."); } try { Types.subtypeOf(boolean.class); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Unexpected primitive boolean. Use the boxed type."); } } @Test public void getFieldJsonQualifierAnnotations_privateFieldTest() { Set annotations = Types.getFieldJsonQualifierAnnotations(ClassWithAnnotatedFields.class, "privateField"); assertThat(annotations).hasSize(1); assertThat(annotations.iterator().next()).isInstanceOf(FieldAnnotation.class); } @Test public void getFieldJsonQualifierAnnotations_publicFieldTest() { Set annotations = Types.getFieldJsonQualifierAnnotations(ClassWithAnnotatedFields.class, "publicField"); assertThat(annotations).hasSize(1); assertThat(annotations.iterator().next()).isInstanceOf(FieldAnnotation.class); } @Test public void getFieldJsonQualifierAnnotations_unannotatedTest() { Set annotations = Types.getFieldJsonQualifierAnnotations(ClassWithAnnotatedFields.class, "unannotatedField"); assertThat(annotations).hasSize(0); } @Test public void generatedJsonAdapterName_strings() { assertThat(Types.generatedJsonAdapterName("com.foo.Test")).isEqualTo("com.foo.TestJsonAdapter"); assertThat(Types.generatedJsonAdapterName("com.foo.Test$Bar")) .isEqualTo("com.foo.Test_BarJsonAdapter"); } @Test public void generatedJsonAdapterName_class() { assertThat(Types.generatedJsonAdapterName(TestJsonClass.class)) .isEqualTo("com.squareup.moshi.TypesTest_TestJsonClassJsonAdapter"); } @Test public void generatedJsonAdapterName_class_missingJsonClass() { try { Types.generatedJsonAdapterName(TestNonJsonClass.class); fail(); } catch (IllegalArgumentException e) { assertThat(e).hasMessageThat().contains("Class does not have a JsonClass annotation"); } } // // Regression tests for https://github.com/square/moshi/issues/338 // // Adapted from https://github.com/google/gson/pull/1128 // private static final class RecursiveTypeVars { RecursiveTypeVars superType; } @Test public void recursiveTypeVariablesResolve() { JsonAdapter> adapter = new Moshi.Builder() .build() .adapter( Types.newParameterizedTypeWithOwner( TypesTest.class, RecursiveTypeVars.class, String.class)); assertThat(adapter).isNotNull(); } @Test public void recursiveTypeVariablesResolve1() { JsonAdapter adapter = new Moshi.Builder().build().adapter(TestType.class); assertThat(adapter).isNotNull(); } @Test public void recursiveTypeVariablesResolve2() { JsonAdapter adapter = new Moshi.Builder().build().adapter(TestType2.class); assertThat(adapter).isNotNull(); } private static class TestType { TestType superType; } private static class TestType2 { TestType2 superReversedType; } @JsonClass(generateAdapter = false) static class TestJsonClass {} static class TestNonJsonClass {} @JsonQualifier @Target(FIELD) @Retention(RUNTIME) @interface FieldAnnotation {} @Target(FIELD) @Retention(RUNTIME) @interface NoQualifierAnnotation {} static class ClassWithAnnotatedFields { @FieldAnnotation @NoQualifierAnnotation private final int privateField = 0; @FieldAnnotation @NoQualifierAnnotation public final int publicField = 0; private final int unannotatedField = 0; } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/internal/ClassJsonAdapterTest.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.internal; import static com.google.common.truth.Truth.assertThat; import static com.squareup.moshi.TestUtil.newReader; import static com.squareup.moshi.internal.Util.NO_ANNOTATIONS; import static org.junit.Assert.fail; import com.squareup.moshi.Json; 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.ByteArrayOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.SimpleTimeZone; import okio.Buffer; import org.junit.Test; public final class ClassJsonAdapterTest { private final Moshi moshi = new Moshi.Builder().build(); static class BasicPizza { int diameter; boolean extraCheese; } @Test public void basicClassAdapter() throws Exception { BasicPizza value = new BasicPizza(); value.diameter = 13; value.extraCheese = true; String toJson = toJson(BasicPizza.class, value); assertThat(toJson).isEqualTo("{\"diameter\":13,\"extraCheese\":true}"); BasicPizza fromJson = fromJson(BasicPizza.class, "{\"diameter\":13,\"extraCheese\":true}"); assertThat(fromJson.diameter).isEqualTo(13); assertThat(fromJson.extraCheese).isTrue(); } static class PrivateFieldsPizza { private String secretIngredient; } @Test public void privateFields() throws Exception { PrivateFieldsPizza value = new PrivateFieldsPizza(); value.secretIngredient = "vodka"; String toJson = toJson(PrivateFieldsPizza.class, value); assertThat(toJson).isEqualTo("{\"secretIngredient\":\"vodka\"}"); PrivateFieldsPizza fromJson = fromJson(PrivateFieldsPizza.class, "{\"secretIngredient\":\"vodka\"}"); assertThat(fromJson.secretIngredient).isEqualTo("vodka"); } static class BasePizza { int diameter; } static class DessertPizza extends BasePizza { boolean chocolate; } @Test public void typeHierarchy() throws Exception { DessertPizza value = new DessertPizza(); value.diameter = 13; value.chocolate = true; String toJson = toJson(DessertPizza.class, value); assertThat(toJson).isEqualTo("{\"chocolate\":true,\"diameter\":13}"); DessertPizza fromJson = fromJson(DessertPizza.class, "{\"diameter\":13,\"chocolate\":true}"); assertThat(fromJson.diameter).isEqualTo(13); assertThat(fromJson.chocolate).isTrue(); } static class BaseAbcde { int d; int a; int c; } static class ExtendsBaseAbcde extends BaseAbcde { int b; int e; } @Test public void fieldsAreAlphabeticalAcrossFlattenedHierarchy() throws Exception { ExtendsBaseAbcde value = new ExtendsBaseAbcde(); value.a = 4; value.b = 5; value.c = 6; value.d = 7; value.e = 8; String toJson = toJson(ExtendsBaseAbcde.class, value); assertThat(toJson).isEqualTo("{\"a\":4,\"b\":5,\"c\":6,\"d\":7,\"e\":8}"); ExtendsBaseAbcde fromJson = fromJson(ExtendsBaseAbcde.class, "{\"a\":4,\"b\":5,\"c\":6,\"d\":7,\"e\":8}"); assertThat(fromJson.a).isEqualTo(4); assertThat(fromJson.b).isEqualTo(5); assertThat(fromJson.c).isEqualTo(6); assertThat(fromJson.d).isEqualTo(7); assertThat(fromJson.e).isEqualTo(8); } static class StaticFields { static int a = 11; int b; } @Test public void staticFieldsOmitted() throws Exception { StaticFields value = new StaticFields(); value.b = 12; String toJson = toJson(StaticFields.class, value); assertThat(toJson).isEqualTo("{\"b\":12}"); StaticFields fromJson = fromJson(StaticFields.class, "{\"a\":13,\"b\":12}"); assertThat(StaticFields.a).isEqualTo(11); // Unchanged. assertThat(fromJson.b).isEqualTo(12); } static class TransientFields { transient int a; int b; } @Test public void transientFieldsOmitted() throws Exception { TransientFields value = new TransientFields(); value.a = 11; value.b = 12; String toJson = toJson(TransientFields.class, value); assertThat(toJson).isEqualTo("{\"b\":12}"); TransientFields fromJson = fromJson(TransientFields.class, "{\"a\":13,\"b\":12}"); assertThat(fromJson.a).isEqualTo(0); // Not assigned. assertThat(fromJson.b).isEqualTo(12); } static class IgnoredFields { @Json(ignore = true) int a; int b; } @Test public void ignoredFieldsOmitted() throws Exception { IgnoredFields value = new IgnoredFields(); value.a = 11; value.b = 12; String toJson = toJson(IgnoredFields.class, value); assertThat(toJson).isEqualTo("{\"b\":12}"); IgnoredFields fromJson = fromJson(IgnoredFields.class, "{\"a\":13,\"b\":12}"); assertThat(fromJson.a).isEqualTo(0); // Not assigned. assertThat(fromJson.b).isEqualTo(12); } static class BaseA { int a; } static class ExtendsBaseA extends BaseA { int a; } @Test public void fieldNameCollision() throws Exception { try { ClassJsonAdapter.Factory.create(ExtendsBaseA.class, NO_ANNOTATIONS, moshi); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Conflicting fields:\n" + " int com.squareup.moshi.internal.ClassJsonAdapterTest$ExtendsBaseA.a\n" + " int com.squareup.moshi.internal.ClassJsonAdapterTest$BaseA.a"); } } static class NameCollision { String foo; @Json(name = "foo") String bar; } @Test public void jsonAnnotationNameCollision() throws Exception { try { ClassJsonAdapter.Factory.create(NameCollision.class, NO_ANNOTATIONS, moshi); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Conflicting fields:\n" + " java.lang.String com.squareup.moshi.internal.ClassJsonAdapterTest$NameCollision.foo\n" + " java.lang.String com.squareup.moshi.internal.ClassJsonAdapterTest$NameCollision.bar"); } } static class TransientBaseA { transient int a; } static class ExtendsTransientBaseA extends TransientBaseA { int a; } @Test public void fieldNameCollisionWithTransientFieldIsOkay() throws Exception { ExtendsTransientBaseA value = new ExtendsTransientBaseA(); value.a = 11; ((TransientBaseA) value).a = 12; String toJson = toJson(ExtendsTransientBaseA.class, value); assertThat(toJson).isEqualTo("{\"a\":11}"); ExtendsTransientBaseA fromJson = fromJson(ExtendsTransientBaseA.class, "{\"a\":11}"); assertThat(fromJson.a).isEqualTo(11); assertThat(((TransientBaseA) fromJson).a).isEqualTo(0); // Not assigned. } static class NoArgConstructor { int a; int b; NoArgConstructor() { a = 5; } } @Test public void noArgConstructor() throws Exception { NoArgConstructor fromJson = fromJson(NoArgConstructor.class, "{\"b\":8}"); assertThat(fromJson.a).isEqualTo(5); assertThat(fromJson.b).isEqualTo(8); } static class NoArgConstructorThrowsCheckedException { NoArgConstructorThrowsCheckedException() throws Exception { throw new Exception("foo"); } } @Test public void noArgConstructorThrowsCheckedException() throws Exception { try { fromJson(NoArgConstructorThrowsCheckedException.class, "{}"); fail(); } catch (RuntimeException expected) { assertThat(expected.getCause()).hasMessageThat().isEqualTo("foo"); } } static class NoArgConstructorThrowsUncheckedException { NoArgConstructorThrowsUncheckedException() throws Exception { throw new UnsupportedOperationException("foo"); } } @Test public void noArgConstructorThrowsUncheckedException() throws Exception { try { fromJson(NoArgConstructorThrowsUncheckedException.class, "{}"); fail(); } catch (UnsupportedOperationException expected) { assertThat(expected).hasMessageThat().isEqualTo("foo"); } } static class NoArgConstructorWithDefaultField { int a = 5; int b; } @Test public void noArgConstructorFieldDefaultsHonored() throws Exception { NoArgConstructorWithDefaultField fromJson = fromJson(NoArgConstructorWithDefaultField.class, "{\"b\":8}"); assertThat(fromJson.a).isEqualTo(5); assertThat(fromJson.b).isEqualTo(8); } static class MagicConstructor { int a; public MagicConstructor(Void argument) { throw new AssertionError(); } } @Test public void magicConstructor() throws Exception { MagicConstructor fromJson = fromJson(MagicConstructor.class, "{\"a\":8}"); assertThat(fromJson.a).isEqualTo(8); } static class MagicConstructorWithDefaultField { int a = 5; int b; public MagicConstructorWithDefaultField(Void argument) { throw new AssertionError(); } } @Test public void magicConstructorFieldDefaultsNotHonored() throws Exception { MagicConstructorWithDefaultField fromJson = fromJson(MagicConstructorWithDefaultField.class, "{\"b\":3}"); assertThat(fromJson.a).isEqualTo(0); // Surprising! No value is assigned. assertThat(fromJson.b).isEqualTo(3); } static class NullRootObject { int a; } @Test public void nullRootObject() throws Exception { String toJson = toJson(PrivateFieldsPizza.class, null); assertThat(toJson).isEqualTo("null"); NullRootObject fromJson = fromJson(NullRootObject.class, "null"); assertThat(fromJson).isNull(); } static class NullFieldValue { String a = "not null"; } @Test public void nullFieldValues() throws Exception { NullFieldValue value = new NullFieldValue(); value.a = null; String toJson = toJson(NullFieldValue.class, value); assertThat(toJson).isEqualTo("{\"a\":null}"); NullFieldValue fromJson = fromJson(NullFieldValue.class, "{\"a\":null}"); assertThat(fromJson.a).isNull(); } class NonStatic {} @Test public void nonStaticNestedClassNotSupported() throws Exception { try { ClassJsonAdapter.Factory.create(NonStatic.class, NO_ANNOTATIONS, moshi); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Cannot serialize non-static nested class " + "com.squareup.moshi.internal.ClassJsonAdapterTest$NonStatic"); } } @Test public void anonymousClassNotSupported() throws Exception { Comparator c = new Comparator() { @Override public int compare(Object a, Object b) { return 0; } }; try { ClassJsonAdapter.Factory.create(c.getClass(), NO_ANNOTATIONS, moshi); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Cannot serialize anonymous class " + c.getClass().getName()); } } @Test public void localClassNotSupported() throws Exception { class Local {} try { ClassJsonAdapter.Factory.create(Local.class, NO_ANNOTATIONS, moshi); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Cannot serialize local class " + "com.squareup.moshi.internal.ClassJsonAdapterTest$1Local"); } } interface Interface {} @Test public void interfaceNotSupported() throws Exception { assertThat(ClassJsonAdapter.Factory.create(Interface.class, NO_ANNOTATIONS, moshi)).isNull(); } abstract static class Abstract {} @Test public void abstractClassNotSupported() throws Exception { try { ClassJsonAdapter.Factory.create(Abstract.class, NO_ANNOTATIONS, moshi); fail(); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Cannot serialize abstract class " + "com.squareup.moshi.internal.ClassJsonAdapterTest$Abstract"); } } static class ExtendsPlatformClassWithPrivateField extends SimpleTimeZone { int a; public ExtendsPlatformClassWithPrivateField() { super(0, "FOO"); } } @Test public void platformSuperclassPrivateFieldIsExcluded() throws Exception { ExtendsPlatformClassWithPrivateField value = new ExtendsPlatformClassWithPrivateField(); value.a = 4; String toJson = toJson(ExtendsPlatformClassWithPrivateField.class, value); assertThat(toJson).isEqualTo("{\"a\":4}"); ExtendsPlatformClassWithPrivateField fromJson = fromJson(ExtendsPlatformClassWithPrivateField.class, "{\"a\":4,\"ID\":\"BAR\"}"); assertThat(fromJson.a).isEqualTo(4); assertThat(fromJson.getID()).isEqualTo("FOO"); } static class ExtendsPlatformClassWithProtectedField extends ByteArrayOutputStream { int a; public ExtendsPlatformClassWithProtectedField() { super(2); } } @Test public void platformSuperclassProtectedFieldIsIncluded() throws Exception { ExtendsPlatformClassWithProtectedField value = new ExtendsPlatformClassWithProtectedField(); value.a = 4; value.write(5); value.write(6); String toJson = toJson(ExtendsPlatformClassWithProtectedField.class, value); assertThat(toJson).isEqualTo("{\"a\":4,\"buf\":[5,6],\"count\":2}"); ExtendsPlatformClassWithProtectedField fromJson = fromJson( ExtendsPlatformClassWithProtectedField.class, "{\"a\":4,\"buf\":[5,6],\"count\":2}"); assertThat(fromJson.a).isEqualTo(4); assertThat(fromJson.toByteArray()).asList().containsExactly((byte) 5, (byte) 6).inOrder(); } static class NamedFields { @Json(name = "#") List phoneNumbers; @Json(name = "@") String emailAddress; @Json(name = "zip code") String zipCode; } @Test public void jsonAnnotationHonored() throws Exception { NamedFields value = new NamedFields(); value.phoneNumbers = Arrays.asList("8005553333", "8005554444"); value.emailAddress = "cash@square.com"; value.zipCode = "94043"; String toJson = toJson(NamedFields.class, value); assertThat(toJson) .isEqualTo( "{" + "\"#\":[\"8005553333\",\"8005554444\"]," + "\"@\":\"cash@square.com\"," + "\"zip code\":\"94043\"" + "}"); NamedFields fromJson = fromJson( NamedFields.class, "{" + "\"#\":[\"8005553333\",\"8005554444\"]," + "\"@\":\"cash@square.com\"," + "\"zip code\":\"94043\"" + "}"); assertThat(fromJson.phoneNumbers).isEqualTo(Arrays.asList("8005553333", "8005554444")); assertThat(fromJson.emailAddress).isEqualTo("cash@square.com"); assertThat(fromJson.zipCode).isEqualTo("94043"); } static final class Box { final T data; Box(T data) { this.data = data; } } @Test public void parameterizedType() throws Exception { @SuppressWarnings("unchecked") JsonAdapter> adapter = (JsonAdapter>) ClassJsonAdapter.Factory.create( Types.newParameterizedTypeWithOwner( ClassJsonAdapterTest.class, Box.class, Integer.class), NO_ANNOTATIONS, moshi); assertThat(adapter.fromJson("{\"data\":5}").data).isEqualTo(5); assertThat(adapter.toJson(new Box<>(5))).isEqualTo("{\"data\":5}"); } private String toJson(Class type, T value) throws IOException { @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. JsonAdapter jsonAdapter = (JsonAdapter) ClassJsonAdapter.Factory.create(type, NO_ANNOTATIONS, moshi); // Wrap in an array to avoid top-level object warnings without going completely lenient. Buffer buffer = new Buffer(); JsonWriter jsonWriter = JsonWriter.of(buffer); jsonWriter.setSerializeNulls(true); jsonWriter.beginArray(); jsonAdapter.toJson(jsonWriter, value); jsonWriter.endArray(); assertThat(buffer.readByte()).isEqualTo((byte) '['); String json = buffer.readUtf8(buffer.size() - 1); assertThat(buffer.readByte()).isEqualTo((byte) ']'); return json; } private T fromJson(Class type, String json) throws IOException { @SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument. JsonAdapter jsonAdapter = (JsonAdapter) ClassJsonAdapter.Factory.create(type, NO_ANNOTATIONS, moshi); // Wrap in an array to avoid top-level object warnings without going completely lenient. JsonReader jsonReader = newReader("[" + json + "]"); jsonReader.beginArray(); T result = jsonAdapter.fromJson(jsonReader); jsonReader.endArray(); return result; } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/internal/JsonValueSourceTest.java ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi.internal; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import java.io.EOFException; import java.io.IOException; import okio.Buffer; import org.junit.Test; public final class JsonValueSourceTest { @Test public void simpleValues() throws IOException { assertThat(jsonPrefix("{\"hello\": \"world\"}, 1, 2, 3")).isEqualTo("{\"hello\": \"world\"}"); assertThat(jsonPrefix("['hello', 'world'], 1, 2, 3")).isEqualTo("['hello', 'world']"); assertThat(jsonPrefix("\"hello\", 1, 2, 3")).isEqualTo("\"hello\""); } @Test public void braceMatching() throws IOException { assertThat(jsonPrefix("[{},{},[],[{}]],[]")).isEqualTo("[{},{},[],[{}]]"); assertThat(jsonPrefix("[\"a\",{\"b\":{\"c\":{\"d\":[\"e\"]}}}],[]")) .isEqualTo("[\"a\",{\"b\":{\"c\":{\"d\":[\"e\"]}}}]"); } @Test public void stringEscapes() throws IOException { assertThat(jsonPrefix("[\"12\\u00334\"],[]")).isEqualTo("[\"12\\u00334\"]"); assertThat(jsonPrefix("[\"12\\n34\"],[]")).isEqualTo("[\"12\\n34\"]"); assertThat(jsonPrefix("[\"12\\\"34\"],[]")).isEqualTo("[\"12\\\"34\"]"); assertThat(jsonPrefix("[\"12\\'34\"],[]")).isEqualTo("[\"12\\'34\"]"); assertThat(jsonPrefix("[\"12\\\\34\"],[]")).isEqualTo("[\"12\\\\34\"]"); assertThat(jsonPrefix("[\"12\\\\\"],[]")).isEqualTo("[\"12\\\\\"]"); } @Test public void bracesInStrings() throws IOException { assertThat(jsonPrefix("[\"]\"],[]")).isEqualTo("[\"]\"]"); assertThat(jsonPrefix("[\"\\]\"],[]")).isEqualTo("[\"\\]\"]"); assertThat(jsonPrefix("[\"\\[\"],[]")).isEqualTo("[\"\\[\"]"); } @Test public void unterminatedString() throws IOException { try { jsonPrefix("{\"a\":\"b..."); fail(); } catch (EOFException expected) { } } @Test public void unterminatedObject() throws IOException { try { jsonPrefix("{\"a\":\"b\",\"c\":"); fail(); } catch (EOFException expected) { } try { jsonPrefix("{"); fail(); } catch (EOFException expected) { } } @Test public void unterminatedArray() throws IOException { try { jsonPrefix("[\"a\",\"b\",\"c\","); fail(); } catch (EOFException expected) { } try { jsonPrefix("["); fail(); } catch (EOFException expected) { } } @Test public void lenientUnterminatedSingleQuotedString() throws IOException { try { jsonPrefix("{\"a\":'b..."); fail(); } catch (EOFException expected) { } } @Test public void emptyStream() throws IOException { try { jsonPrefix(""); fail(); } catch (EOFException expected) { } try { jsonPrefix(" "); fail(); } catch (EOFException expected) { } try { jsonPrefix("/* comment */"); fail(); } catch (EOFException expected) { } } @Test public void lenientSingleQuotedStrings() throws IOException { assertThat(jsonPrefix("['hello', 'world'], 1, 2, 3")).isEqualTo("['hello', 'world']"); assertThat(jsonPrefix("'abc\\'', 123")).isEqualTo("'abc\\''"); } @Test public void lenientCStyleComments() throws IOException { assertThat(jsonPrefix("[\"a\"/* \"b\" */,\"c\"],[]")).isEqualTo("[\"a\"/* \"b\" */,\"c\"]"); assertThat(jsonPrefix("[\"a\"/*]*/],[]")).isEqualTo("[\"a\"/*]*/]"); assertThat(jsonPrefix("[\"a\"/**/],[]")).isEqualTo("[\"a\"/**/]"); assertThat(jsonPrefix("[\"a\"/*/ /*/],[]")).isEqualTo("[\"a\"/*/ /*/]"); assertThat(jsonPrefix("[\"a\"/*/ **/],[]")).isEqualTo("[\"a\"/*/ **/]"); } @Test public void lenientEndOfLineComments() throws IOException { assertThat(jsonPrefix("[\"a\"// \"b\" \n,\"c\"],[]")).isEqualTo("[\"a\"// \"b\" \n,\"c\"]"); assertThat(jsonPrefix("[\"a\"// \"b\" \r\n,\"c\"],[]")).isEqualTo("[\"a\"// \"b\" \r\n,\"c\"]"); assertThat(jsonPrefix("[\"a\"// \"b\" \r,\"c\"],[]")).isEqualTo("[\"a\"// \"b\" \r,\"c\"]"); assertThat(jsonPrefix("[\"a\"//]\r\n\"c\"],[]")).isEqualTo("[\"a\"//]\r\n\"c\"]"); } @Test public void lenientSlashInToken() throws IOException { assertThat(jsonPrefix("{a/b:\"c\"},[]")).isEqualTo("{a/b:\"c\"}"); } @Test public void lenientUnterminatedEndOfLineComment() throws IOException { try { jsonPrefix("{\"a\",//}"); fail(); } catch (EOFException expected) { } } @Test public void lenientUnterminatedCStyleComment() throws IOException { try { jsonPrefix("{\"a\",/* *"); fail(); } catch (EOFException expected) { } try { jsonPrefix("{\"a\",/* **"); fail(); } catch (EOFException expected) { } try { jsonPrefix("{\"a\",/* /**"); fail(); } catch (EOFException expected) { } } @Test public void discard() throws IOException { Buffer allData = new Buffer(); allData.writeUtf8("{\"a\",\"b\",\"c\"},[\"d\", \"e\"]"); JsonValueSource jsonValueSource = new JsonValueSource(allData); jsonValueSource.close(); jsonValueSource.discard(); assertThat(allData.readUtf8()).isEqualTo(",[\"d\", \"e\"]"); } private String jsonPrefix(String string) throws IOException { Buffer allData = new Buffer(); allData.writeUtf8(string); Buffer jsonPrefixBuffer = new Buffer(); jsonPrefixBuffer.writeAll(new JsonValueSource(allData)); String result = jsonPrefixBuffer.readUtf8(); String remainder = allData.readUtf8(); assertThat(result + remainder).isEqualTo(string); return result; } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/internal/KotlinReflectTypesTest.kt ================================================ /* * Copyright (C) 2025 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 * * https://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.squareup.moshi.internal import assertk.Assert import assertk.assertFailure import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isInstanceOf import assertk.assertions.isNull import com.squareup.moshi.internal.javaType as moshiJavaType import com.squareup.moshi.rawType import java.lang.reflect.GenericArrayType import java.lang.reflect.GenericDeclaration import java.lang.reflect.ParameterizedType import java.lang.reflect.TypeVariable import java.lang.reflect.WildcardType import kotlin.reflect.KFunction0 import kotlin.reflect.javaType as kotlinJavaType import kotlin.reflect.typeOf import org.junit.Test class KotlinReflectTypesTest { @Test fun regularClass() { val kotlinType = typeOf() val javaType = kotlinType.moshiJavaType as Class<*> assertThat(javaType).isSymmetricEqualTo(kotlinType.kotlinJavaType) assertThat(javaType).isEqualTo(String::class.java) } @Test fun regularArray() { val kotlinType = typeOf>() val javaType = kotlinType.moshiJavaType assertThat(javaType).isSymmetricEqualTo(kotlinType.kotlinJavaType) assertThat(javaType).isEqualTo(Array::class.java) } @Test fun varianceInArray() { val kotlinType = typeOf>() val javaType = kotlinType.moshiJavaType assertThat(javaType).isSymmetricEqualTo(kotlinType.kotlinJavaType) assertThat(javaType).isEqualTo(Array::class.java) } @Test fun varianceOutArray() { val kotlinType = typeOf>() val javaType = kotlinType.moshiJavaType assertThat(javaType).isSymmetricEqualTo(kotlinType.kotlinJavaType) assertThat(javaType).isEqualTo(Array::class.java) } @Test fun genericArray() { val kotlinType = typeOf>>() val javaType = kotlinType.moshiJavaType as GenericArrayType assertThat(javaType).isSymmetricEqualTo(kotlinType.kotlinJavaType) assertThat(javaType.toString()).isEqualTo("java.util.List[]") val componentType = javaType.genericComponentType as ParameterizedType assertThat(componentType.rawType).isEqualTo(List::class.java) assertThat(componentType.actualTypeArguments).containsExactly(String::class.java) assertThat(componentType.ownerType).isNull() } @Test fun parameterizedType() { val kotlinType = typeOf>() val javaType = kotlinType.moshiJavaType as ParameterizedType assertThat(javaType).isSymmetricEqualTo(kotlinType.kotlinJavaType) assertThat(javaType.toString()).isEqualTo("java.util.List") assertThat(javaType.rawType).isEqualTo(List::class.java) assertThat(javaType.actualTypeArguments).containsExactly(String::class.java) assertThat(javaType.ownerType).isNull() } @Test fun outWildcardType() { val kotlinType = typeOf>() val javaType = kotlinType.moshiJavaType as ParameterizedType assertThat(javaType).isSymmetricEqualTo(kotlinType.kotlinJavaType) assertThat(javaType.toString()).isEqualTo("java.util.List") assertThat(javaType.rawType).isEqualTo(List::class.java) val wildcardType = javaType.actualTypeArguments.single() as WildcardType assertThat(wildcardType.rawType).isEqualTo(String::class.java) assertThat(wildcardType.upperBounds).containsExactly(String::class.java) assertThat(wildcardType.lowerBounds).isEmpty() } @Test fun inWildcardType() { val kotlinType = typeOf>() val javaType = kotlinType.moshiJavaType as ParameterizedType assertThat(javaType).isSymmetricEqualTo(kotlinType.kotlinJavaType) assertThat(javaType.toString()).isEqualTo("java.util.List") assertThat(javaType.rawType).isEqualTo(List::class.java) val wildcardType = javaType.actualTypeArguments.single() as WildcardType assertThat(wildcardType.rawType).isEqualTo(Any::class.java) assertThat(wildcardType.upperBounds).containsExactly(Any::class.java) assertThat(wildcardType.lowerBounds).containsExactly(String::class.java) } @Test fun starWildcardType() { val kotlinType = typeOf>() val javaType = kotlinType.moshiJavaType as ParameterizedType assertThat(javaType).isSymmetricEqualTo(kotlinType.kotlinJavaType) assertThat(javaType.toString()).isEqualTo("java.util.List") assertThat(javaType.rawType).isEqualTo(List::class.java) val wildcardType = javaType.actualTypeArguments.single() as WildcardType assertThat(wildcardType.rawType).isEqualTo(Any::class.java) assertThat(wildcardType.upperBounds).containsExactly(Any::class.java) assertThat(wildcardType.lowerBounds).isEmpty() } @Test fun primitiveType() { val kotlinType = typeOf() val javaType = kotlinType.moshiJavaType as Class<*> assertThat(javaType).isSymmetricEqualTo(kotlinType.kotlinJavaType) assertThat(javaType).isEqualTo(Int::class.java) } @Test fun typeVariable() { val function: KFunction0 = ::hello val kotlinType = function.returnType val javaType = kotlinType.moshiJavaType as TypeVariable assertThat(javaType.bounds).containsExactly(Any::class.java) assertFailure { javaType.genericDeclaration }.isInstanceOf() assertThat(javaType.name).isEqualTo("T") assertThat(javaType.toString()).isEqualTo("T") } fun hello(): T { error("Unexpected call") } fun Assert.isSymmetricEqualTo(expected: T) = given { actual -> assertThat(actual).isEqualTo(expected) assertThat(actual.hashCode(), "hashCode()").isEqualTo(expected.hashCode()) assertThat(expected, "symmetric equals").isEqualTo(actual) } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/internal/LinkedHashTreeMapTest.java ================================================ /* * Copyright (C) 2012 Google 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.squareup.moshi.internal; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import com.squareup.moshi.internal.LinkedHashTreeMap.Node; import java.util.Iterator; import java.util.Map; import java.util.Random; import org.junit.Test; @SuppressWarnings("KotlinInternalInJava") public final class LinkedHashTreeMapTest { @Test public void iterationOrder() { LinkedHashTreeMap map = new LinkedHashTreeMap<>(); map.put("a", "android"); map.put("c", "cola"); map.put("b", "bbq"); assertThat(map.keySet()).containsExactly("a", "c", "b"); assertThat(map.values()).containsExactly("android", "cola", "bbq"); } @Test public void removeRootDoesNotDoubleUnlink() { LinkedHashTreeMap map = new LinkedHashTreeMap<>(); map.put("a", "android"); map.put("c", "cola"); map.put("b", "bbq"); Iterator> it = map.entrySet().iterator(); it.next(); it.next(); it.next(); it.remove(); assertThat(map.keySet()).containsExactly("a", "c"); } @Test public void putNullKeyFails() { LinkedHashTreeMap map = new LinkedHashTreeMap<>(); try { map.put(null, "android"); fail(); } catch (NullPointerException expected) { } } @Test public void putNonComparableKeyFails() { LinkedHashTreeMap map = new LinkedHashTreeMap<>(); try { map.put(new Object(), "android"); fail(); } catch (ClassCastException expected) { } } @Test public void ContainsNonComparableKeyReturnsFalse() { LinkedHashTreeMap map = new LinkedHashTreeMap<>(); map.put("a", "android"); assertThat(map).doesNotContainKey(new Object()); } @Test public void containsNullKeyIsAlwaysFalse() { LinkedHashTreeMap map = new LinkedHashTreeMap<>(); map.put("a", "android"); assertThat(map).doesNotContainKey(null); } @Test public void putOverrides() throws Exception { LinkedHashTreeMap map = new LinkedHashTreeMap<>(); assertThat(map.put("d", "donut")).isNull(); assertThat(map.put("e", "eclair")).isNull(); assertThat(map.put("f", "froyo")).isNull(); assertThat(map.size()).isEqualTo(3); assertThat(map.get("d")).isEqualTo("donut"); assertThat(map.put("d", "done")).isEqualTo("donut"); assertThat(map).hasSize(3); } @Test public void emptyStringValues() { LinkedHashTreeMap map = new LinkedHashTreeMap<>(); map.put("a", ""); assertThat(map.containsKey("a")).isTrue(); assertThat(map.get("a")).isEqualTo(""); } // NOTE that this does not happen every time, but given the below predictable random, // this test will consistently fail (assuming the initial size is 16 and rehashing // size remains at 3/4) @Test public void forceDoublingAndRehash() throws Exception { Random random = new Random(1367593214724L); LinkedHashTreeMap map = new LinkedHashTreeMap<>(); String[] keys = new String[1000]; for (int i = 0; i < keys.length; i++) { keys[i] = Integer.toString(Math.abs(random.nextInt()), 36) + "-" + i; map.put(keys[i], "" + i); } for (int i = 0; i < keys.length; i++) { String key = keys[i]; assertThat(map.containsKey(key)).isTrue(); assertThat(map.get(key)).isEqualTo("" + i); } } @Test public void clear() { LinkedHashTreeMap map = new LinkedHashTreeMap<>(); map.put("a", "android"); map.put("c", "cola"); map.put("b", "bbq"); map.clear(); assertThat(map.keySet()).containsExactly(); assertThat(map).isEmpty(); } @Test public void equalsAndHashCode() throws Exception { LinkedHashTreeMap map1 = new LinkedHashTreeMap<>(); map1.put("A", 1); map1.put("B", 2); map1.put("C", 3); map1.put("D", 4); LinkedHashTreeMap map2 = new LinkedHashTreeMap<>(); map2.put("C", 3); map2.put("B", 2); map2.put("D", 4); map2.put("A", 1); assertThat(map2).isEqualTo(map1); assertThat(map2.hashCode()).isEqualTo(map1.hashCode()); } @Test public void avlWalker() { assertAvlWalker(node(node("a"), "b", node("c")), "a", "b", "c"); assertAvlWalker( node(node(node("a"), "b", node("c")), "d", node(node("e"), "f", node("g"))), "a", "b", "c", "d", "e", "f", "g"); assertAvlWalker( node(node(null, "a", node("b")), "c", node(node("d"), "e", null)), "a", "b", "c", "d", "e"); assertAvlWalker( node(null, "a", node(null, "b", node(null, "c", node("d")))), "a", "b", "c", "d"); assertAvlWalker( node(node(node(node("a"), "b", null), "c", null), "d", null), "a", "b", "c", "d"); } private void assertAvlWalker(Node root, String... values) { AvlIterator iterator = new AvlIterator<>(); iterator.reset(root); for (String value : values) { assertThat(iterator.next().getKey()).isEqualTo(value); } assertThat(iterator.next()).isNull(); } @Test public void avlBuilder() { assertAvlBuilder(1, "a"); assertAvlBuilder(2, "(. a b)"); assertAvlBuilder(3, "(a b c)"); assertAvlBuilder(4, "(a b (. c d))"); assertAvlBuilder(5, "(a b (c d e))"); assertAvlBuilder(6, "((. a b) c (d e f))"); assertAvlBuilder(7, "((a b c) d (e f g))"); assertAvlBuilder(8, "((a b c) d (e f (. g h)))"); assertAvlBuilder(9, "((a b c) d (e f (g h i)))"); assertAvlBuilder(10, "((a b c) d ((. e f) g (h i j)))"); assertAvlBuilder(11, "((a b c) d ((e f g) h (i j k)))"); assertAvlBuilder(12, "((a b (. c d)) e ((f g h) i (j k l)))"); assertAvlBuilder(13, "((a b (c d e)) f ((g h i) j (k l m)))"); assertAvlBuilder(14, "(((. a b) c (d e f)) g ((h i j) k (l m n)))"); assertAvlBuilder(15, "(((a b c) d (e f g)) h ((i j k) l (m n o)))"); assertAvlBuilder(16, "(((a b c) d (e f g)) h ((i j k) l (m n (. o p))))"); assertAvlBuilder( 30, "((((. a b) c (d e f)) g ((h i j) k (l m n))) o " + "(((p q r) s (t u v)) w ((x y z) A (B C D))))"); assertAvlBuilder( 31, "((((a b c) d (e f g)) h ((i j k) l (m n o))) p " + "(((q r s) t (u v w)) x ((y z A) B (C D E))))"); } private void assertAvlBuilder(int size, String expected) { char[] values = "abcdefghijklmnopqrstuvwxyzABCDE".toCharArray(); AvlBuilder avlBuilder = new AvlBuilder<>(); avlBuilder.reset(size); for (int i = 0; i < size; i++) { avlBuilder.add(node(Character.toString(values[i]))); } assertTree(expected, avlBuilder.root()); } @Test public void doubleCapacity() { @SuppressWarnings("unchecked") // Arrays and generics don't get along. Node[] oldTable = new Node[1]; oldTable[0] = node(node(node("a"), "b", node("c")), "d", node(node("e"), "f", node("g"))); Node[] newTable = LinkedHashTreeMapKt.doubleCapacity(oldTable); assertTree("(b d f)", newTable[0]); // Even hash codes! assertTree("(a c (. e g))", newTable[1]); // Odd hash codes! } @Test public void doubleCapacityAllNodesOnLeft() { @SuppressWarnings("unchecked") // Arrays and generics don't get along. Node[] oldTable = new Node[1]; oldTable[0] = node(node("b"), "d", node("f")); Node[] newTable = LinkedHashTreeMapKt.doubleCapacity(oldTable); assertTree("(b d f)", newTable[0]); // Even hash codes! assertThat(newTable[1]).isNull(); for (Node node : newTable) { if (node != null) { assertConsistent(node); } } } private static final Node head = new Node<>(); private Node node(String value) { return new Node<>(null, value, value.hashCode(), head, head); } private Node node( Node left, String value, Node right) { Node result = node(value); if (left != null) { result.left = left; left.parent = result; } if (right != null) { result.right = right; right.parent = result; } return result; } private void assertTree(String expected, Node root) { assertThat(toString(root)).isEqualTo(expected); assertConsistent(root); } private void assertConsistent(Node node) { int leftHeight = 0; if (node.left != null) { assertConsistent(node.left); assertThat(node.left.parent).isSameInstanceAs(node); leftHeight = node.left.height; } int rightHeight = 0; if (node.right != null) { assertConsistent(node.right); assertThat(node.right.parent).isSameInstanceAs(node); rightHeight = node.right.height; } if (node.parent != null) { assertThat(node.parent.left == node || node.parent.right == node).isTrue(); } if (Math.max(leftHeight, rightHeight) + 1 != node.height) { fail(); } } private String toString(Node root) { if (root == null) { return "."; } else if (root.left == null && root.right == null) { return String.valueOf(root.getKey()); } else { return String.format("(%s %s %s)", toString(root.left), root.getKey(), toString(root.right)); } } } ================================================ FILE: moshi/src/test/java/com/squareup/moshi/internal/MapJsonAdapterTest.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.internal; import static com.google.common.truth.Truth.assertThat; import static com.squareup.moshi.TestUtil.newReader; import static com.squareup.moshi.internal.Util.NO_ANNOTATIONS; import static org.junit.Assert.fail; 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.reflect.Type; import java.util.ArrayList; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Map; import okio.Buffer; import org.junit.Test; public final class MapJsonAdapterTest { private final Moshi moshi = new Moshi.Builder().build(); @Test public void map() throws Exception { Map map = new LinkedHashMap<>(); map.put("a", true); map.put("b", false); map.put("c", null); String toJson = toJson(String.class, Boolean.class, map); assertThat(toJson).isEqualTo("{\"a\":true,\"b\":false,\"c\":null}"); Map fromJson = fromJson(String.class, Boolean.class, "{\"a\":true,\"b\":false,\"c\":null}"); assertThat(fromJson) .containsExactly( "a", true, "b", false, "c", null); } @Test public void mapWithNullKeyFailsToEmit() throws Exception { Map map = new LinkedHashMap<>(); map.put(null, true); try { toJson(String.class, Boolean.class, map); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Map key is null at $."); } } @Test public void emptyMap() throws Exception { Map map = new LinkedHashMap<>(); String toJson = toJson(String.class, Boolean.class, map); assertThat(toJson).isEqualTo("{}"); Map fromJson = fromJson(String.class, Boolean.class, "{}"); assertThat(fromJson).isEmpty(); } @Test public void nullMap() throws Exception { JsonAdapter jsonAdapter = mapAdapter(String.class, Boolean.class); Buffer buffer = new Buffer(); JsonWriter jsonWriter = JsonWriter.of(buffer); jsonWriter.setLenient(true); jsonAdapter.toJson(jsonWriter, null); assertThat(buffer.readUtf8()).isEqualTo("null"); JsonReader jsonReader = newReader("null"); jsonReader.setLenient(true); assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(null); } @Test public void covariantValue() throws Exception { // Important for Kotlin maps, which are all Map. JsonAdapter> jsonAdapter = mapAdapter(String.class, Types.subtypeOf(Object.class)); Map map = new LinkedHashMap<>(); map.put("boolean", true); map.put("float", 42.0); map.put("String", "value"); String asJson = "{\"boolean\":true,\"float\":42.0,\"String\":\"value\"}"; Buffer buffer = new Buffer(); JsonWriter jsonWriter = JsonWriter.of(buffer); jsonAdapter.toJson(jsonWriter, map); assertThat(buffer.readUtf8()).isEqualTo(asJson); JsonReader jsonReader = newReader(asJson); assertThat(jsonAdapter.fromJson(jsonReader)).isEqualTo(map); } @Test public void orderIsRetained() throws Exception { Map map = new LinkedHashMap<>(); map.put("c", 1); map.put("a", 2); map.put("d", 3); map.put("b", 4); String toJson = toJson(String.class, Integer.class, map); assertThat(toJson).isEqualTo("{\"c\":1,\"a\":2,\"d\":3,\"b\":4}"); Map fromJson = fromJson(String.class, Integer.class, "{\"c\":1,\"a\":2,\"d\":3,\"b\":4}"); assertThat(new ArrayList(fromJson.keySet())) .isEqualTo(Arrays.asList("c", "a", "d", "b")); } @Test public void duplicatesAreForbidden() throws Exception { try { fromJson(String.class, Integer.class, "{\"c\":1,\"c\":2}"); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Map key 'c' has multiple values at path $.c: 1 and 2"); } } /** This leans on {@code promoteNameToValue} to do the heavy lifting. */ @Test public void mapWithNonStringKeys() throws Exception { Map map = new LinkedHashMap<>(); map.put(5, true); map.put(6, false); map.put(7, null); String toJson = toJson(Integer.class, Boolean.class, map); assertThat(toJson).isEqualTo("{\"5\":true,\"6\":false,\"7\":null}"); Map fromJson = fromJson(Integer.class, Boolean.class, "{\"5\":true,\"6\":false,\"7\":null}"); assertThat(fromJson) .containsExactly( 5, true, 6, false, 7, null); } @Test public void mapWithNonStringKeysToJsonObject() { Map map = new LinkedHashMap<>(); map.put(5, true); map.put(6, false); map.put(7, null); Map jsonObject = new LinkedHashMap<>(); jsonObject.put("5", true); jsonObject.put("6", false); jsonObject.put("7", null); JsonAdapter> jsonAdapter = mapAdapter(Integer.class, Boolean.class); assertThat(jsonAdapter.serializeNulls().toJsonValue(map)).isEqualTo(jsonObject); assertThat(jsonAdapter.fromJsonValue(jsonObject)).isEqualTo(map); } @Test public void booleanKeyTypeHasCoherentErrorMessage() { Map map = new LinkedHashMap<>(); map.put(true, ""); JsonAdapter> adapter = mapAdapter(Boolean.class, String.class); try { adapter.toJson(map); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Boolean cannot be used as a map key in JSON at path $."); } try { adapter.toJsonValue(map); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Boolean cannot be used as a map key in JSON at path $."); } } static final class Key {} @Test public void objectKeyTypeHasCoherentErrorMessage() { Map map = new LinkedHashMap<>(); map.put(new Key(), ""); JsonAdapter> adapter = mapAdapter(Key.class, String.class); try { adapter.toJson(map); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Object cannot be used as a map key in JSON at path $."); } try { adapter.toJsonValue(map); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Object cannot be " + "used as a map key in JSON at path $."); } } @Test public void arrayKeyTypeHasCoherentErrorMessage() { Map map = new LinkedHashMap<>(); map.put(new String[0], ""); JsonAdapter> adapter = mapAdapter(Types.arrayOf(String.class), String.class); try { adapter.toJson(map); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Array cannot be used as a map key in JSON at path $."); } try { adapter.toJsonValue(map); fail(); } catch (IllegalStateException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Array cannot be used as a map key in JSON at path $."); } } private String toJson(Type keyType, Type valueType, Map value) throws IOException { JsonAdapter> jsonAdapter = mapAdapter(keyType, valueType); Buffer buffer = new Buffer(); JsonWriter jsonWriter = JsonWriter.of(buffer); jsonWriter.setSerializeNulls(true); jsonAdapter.toJson(jsonWriter, value); return buffer.readUtf8(); } @SuppressWarnings("unchecked") // It's the caller's responsibility to make sure K and V match. private JsonAdapter> mapAdapter(Type keyType, Type valueType) { return (JsonAdapter>) MapJsonAdapter.Factory.create( Types.newParameterizedType(Map.class, keyType, valueType), NO_ANNOTATIONS, moshi); } private Map fromJson(Type keyType, Type valueType, String json) throws IOException { JsonAdapter> mapJsonAdapter = mapAdapter(keyType, valueType); return mapJsonAdapter.fromJson(json); } } ================================================ FILE: moshi-adapters/README.md ================================================ Adapters =================== Prebuilt Moshi `JsonAdapter`s for various things, such as `Rfc3339DateJsonAdapter` for parsing `java.util.Date`s To use, supply an instance of your desired converter when building your `Moshi` instance. ```java Moshi moshi = new Moshi.Builder() .add(Date.class, new Rfc3339DateJsonAdapter()) //etc .build(); ``` Download -------- Download [the latest JAR][1] or grab via [Maven][2]: ```xml com.squareup.moshi moshi-adapters latest.version ``` or [Gradle][2]: ```groovy implementation 'com.squareup.moshi:moshi-adapters:latest.version' ``` Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. [1]: https://search.maven.org/remote_content?g=com.squareup.moshi&a=moshi-adapters&v=LATEST [2]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.squareup.moshi%22%20a%3A%22moshi-adapters%22 [snap]: https://s01.oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/moshi-adapters/ ================================================ FILE: moshi-adapters/build.gradle.kts ================================================ import org.gradle.jvm.tasks.Jar plugins { kotlin("jvm") id("com.vanniktech.maven.publish") id("org.jetbrains.dokka") } dependencies { compileOnly(libs.jsr305) api(project(":moshi")) testImplementation(libs.junit) testImplementation(libs.truth) } tasks.withType().configureEach { manifest { attributes("Automatic-Module-Name" to "com.squareup.moshi.adapters") } } ================================================ FILE: moshi-adapters/japicmp/build.gradle.kts ================================================ import me.champeau.gradle.japicmp.JapicmpTask plugins { `java-library` id("me.champeau.gradle.japicmp") } val baseline = configurations.create("baseline") val latest = configurations.create("latest") dependencies { baseline("com.squareup.moshi:moshi-adapters:1.15.2") { isTransitive = false version { strictly("1.14.0") } } latest(project(":moshi-adapters")) } val japicmp = tasks.register("japicmp") { dependsOn("jar") oldClasspath.from(baseline) newClasspath.from(latest) onlyBinaryIncompatibleModified.set(true) failOnModification.set(true) txtOutputFile.set(layout.buildDirectory.file("reports/japi.txt")) ignoreMissingClasses.set(true) includeSynthetic.set(true) } tasks.named("check").configure { dependsOn(japicmp) } ================================================ FILE: moshi-adapters/src/main/java/com/squareup/moshi/Rfc3339DateJsonAdapter.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter import java.util.Date import okio.IOException @Deprecated( """This class moved to avoid a package name conflict in the Java Platform Module System. The new class is com.squareup.moshi.adapters.Rfc3339DateJsonAdapter.""", replaceWith = ReplaceWith("com.squareup.moshi.adapters.Rfc3339DateJsonAdapter"), level = DeprecationLevel.ERROR, ) public class Rfc3339DateJsonAdapter : JsonAdapter() { private val delegate = Rfc3339DateJsonAdapter() @Throws(IOException::class) override fun fromJson(reader: JsonReader): Date? { return delegate.fromJson(reader) } @Throws(IOException::class) override fun toJson(writer: JsonWriter, value: Date?) { delegate.toJson(writer, value) } } ================================================ FILE: moshi-adapters/src/main/java/com/squareup/moshi/adapters/EnumJsonAdapter.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.adapters import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonReader.Options import com.squareup.moshi.JsonReader.Token.STRING import com.squareup.moshi.JsonWriter import com.squareup.moshi.internal.jsonName import okio.IOException /** * A JsonAdapter for enums that allows having a fallback enum value when a deserialized string does * not match any enum value. To use, add this as an adapter for your enum type on your * [Moshi.Builder][com.squareup.moshi.Moshi.Builder]: * ``` * Moshi moshi = new Moshi.Builder() * .add(CurrencyCode.class, EnumJsonAdapter.create(CurrencyCode.class) * .withUnknownFallback(CurrencyCode.USD)) * .build(); * ``` */ public class EnumJsonAdapter> internal constructor( private val enumType: Class, private val fallbackValue: T?, private val useFallbackValue: Boolean, ) : JsonAdapter() { private val constants = enumType.enumConstants private val nameStrings = Array(constants.size) { i -> val constantName = constants[i].name enumType.getField(constantName).jsonName(constantName) } private val options = Options.of(*nameStrings) /** * Create a new adapter for this enum with a fallback value to use when the JSON string does not * match any of the enum's constants. Note that this value will not be used when the JSON value is * null, absent, or not a string. Also, the string values are case-sensitive, and this fallback * value will be used even on case mismatches. */ public fun withUnknownFallback(fallbackValue: T?): EnumJsonAdapter { return EnumJsonAdapter(enumType, fallbackValue, useFallbackValue = true) } @Throws(IOException::class) override fun fromJson(reader: JsonReader): T? { val index = reader.selectString(options) if (index != -1) return constants[index] if (!useFallbackValue) { val name = reader.nextString() throw JsonDataException( "Expected one of ${nameStrings.toList()} but was $name at path ${reader.path}" ) } if (reader.peek() != STRING) { throw JsonDataException("Expected a string but was ${reader.peek()} at path ${reader.path}") } reader.skipValue() return fallbackValue } @Throws(IOException::class) override fun toJson(writer: JsonWriter, value: T?) { if (value == null) { throw NullPointerException("value was null! Wrap in .nullSafe() to write nullable values.") } writer.value(nameStrings[value.ordinal]) } override fun toString(): String = "EnumJsonAdapter(${enumType.name})" public companion object { @JvmStatic public fun > create(enumType: Class): EnumJsonAdapter { return EnumJsonAdapter(enumType, fallbackValue = null, useFallbackValue = false) } } } ================================================ FILE: moshi-adapters/src/main/java/com/squareup/moshi/adapters/Iso8601Utils.kt ================================================ /* * Copyright (C) 2011 FasterXML, LLC * * 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 * * https://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.squareup.moshi.adapters import com.squareup.moshi.JsonDataException import java.util.Calendar import java.util.Date import java.util.GregorianCalendar import java.util.Locale import java.util.TimeZone import kotlin.math.min import kotlin.math.pow /* * Jackson’s date formatter, pruned to Moshi's needs. Forked from this file: * https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java * * Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC * friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date * objects. * * Supported parse format: * `[yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]]` * * @see [this specification](http://www.w3.org/TR/NOTE-datetime) */ /** ID to represent the 'GMT' string */ private const val GMT_ID = "GMT" /** The GMT timezone, prefetched to avoid more lookups. */ private val TIMEZONE_Z: TimeZone = TimeZone.getTimeZone(GMT_ID) /** Returns `date` formatted as yyyy-MM-ddThh:mm:ss.sssZ */ internal fun Date.formatIsoDate(): String { val calendar: Calendar = GregorianCalendar(TIMEZONE_Z, Locale.US) calendar.time = this // estimate capacity of buffer as close as we can (yeah, that's pedantic ;) val capacity = "yyyy-MM-ddThh:mm:ss.sssZ".length return buildString(capacity) { padInt(calendar[Calendar.YEAR], "yyyy".length) append('-') padInt(calendar[Calendar.MONTH] + 1, "MM".length) append('-') padInt(calendar[Calendar.DAY_OF_MONTH], "dd".length) append('T') padInt(calendar[Calendar.HOUR_OF_DAY], "hh".length) append(':') padInt(calendar[Calendar.MINUTE], "mm".length) append(':') padInt(calendar[Calendar.SECOND], "ss".length) append('.') padInt(calendar[Calendar.MILLISECOND], "sss".length) append('Z') } } /** * Parse a date from ISO-8601 formatted string. It expects a format * `[yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]` * * @return the parsed date * @receiver ISO string to parse in the appropriate format. */ internal fun String.parseIsoDate(): Date { return try { var offset = 0 // extract year val year = readInt( offset, run { offset += 4 offset }, ) if (readChar(this, offset, '-')) { offset += 1 } // extract month val month = readInt( offset, run { offset += 2 offset }, ) if (readChar(this, offset, '-')) { offset += 1 } // extract day val day = readInt( offset, run { offset += 2 offset }, ) // default time value var hour = 0 var minutes = 0 var seconds = 0 // always use 0 otherwise returned date will include millis of current time var milliseconds = 0 // if the value has no time component (and no time zone), we are done val hasT = readChar(this, offset, 'T') if (!hasT && this.length <= offset) { // Note that this uses the host machine's time zone. That's a bug. return GregorianCalendar(year, month - 1, day).time } if (hasT) { offset++ // extract hours, minutes, seconds and milliseconds hour = readInt( offset, run { offset += 2 offset }, ) if (readChar(this, offset, ':')) { offset += 1 } minutes = readInt( offset, run { offset += 2 offset }, ) if (readChar(this, offset, ':')) { offset += 1 } // second and milliseconds can be optional if (this.length > offset) { val c = this[offset] if (c != 'Z' && c != '+' && c != '-') { seconds = readInt( offset, run { offset += 2 offset }, ) if (seconds in 60..62) seconds = 59 // truncate up to 3 leap seconds // milliseconds can be optional in the format if (readChar(this, offset, '.')) { offset += 1 val endOffset = this.indexOfNonDigit(offset + 1) // assume at least one digit val parseEndOffset = min(endOffset, offset + 3) // parse up to 3 digits val fraction = readInt(offset, parseEndOffset) milliseconds = (10.0.pow((3 - (parseEndOffset - offset)).toDouble()) * fraction).toInt() offset = endOffset } } } } // extract timezone require(this.length > offset) { "No time zone indicator" } val timezone: TimeZone val timezoneIndicator = this[offset] if (timezoneIndicator == 'Z') { timezone = TIMEZONE_Z } else if (timezoneIndicator == '+' || timezoneIndicator == '-') { val timezoneOffset = this.substring(offset) // 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00" if ("+0000" == timezoneOffset || "+00:00" == timezoneOffset) { timezone = TIMEZONE_Z } else { // 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC... // not sure why, but it is what it is. val timezoneId = GMT_ID + timezoneOffset timezone = TimeZone.getTimeZone(timezoneId) val act = timezone.id if (act != timezoneId) { /* * 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given * one without. If so, don't sweat. * Yes, very inefficient. Hopefully not hit often. * If it becomes a perf problem, add 'loose' comparison instead. */ val cleaned = act.replace(":", "") if (cleaned != timezoneId) { throw IndexOutOfBoundsException( "Mismatching time zone indicator: $timezoneId given, resolves to ${timezone.id}" ) } } } } else { throw IndexOutOfBoundsException("Invalid time zone indicator '$timezoneIndicator'") } val calendar: Calendar = GregorianCalendar(timezone) calendar.isLenient = false calendar[Calendar.YEAR] = year calendar[Calendar.MONTH] = month - 1 calendar[Calendar.DAY_OF_MONTH] = day calendar[Calendar.HOUR_OF_DAY] = hour calendar[Calendar.MINUTE] = minutes calendar[Calendar.SECOND] = seconds calendar[Calendar.MILLISECOND] = milliseconds calendar.time // If we get a ParseException it'll already have the right message/offset. // Other exception types can convert here. } catch (e: IndexOutOfBoundsException) { throw JsonDataException("Not an RFC 3339 date: $this", e) } catch (e: IllegalArgumentException) { throw JsonDataException("Not an RFC 3339 date: $this", e) } } /** * Check if the expected character exist at the given offset in the value. * * @param value the string to check at the specified offset * @param offset the offset to look for the expected character * @param expected the expected character * @return true if the expected character exist at the given offset */ private fun readChar(value: String, offset: Int, expected: Char): Boolean { return offset < value.length && value[offset] == expected } /** * Parse an integer located between 2 given offsets in a string * * @param beginIndex the start index for the integer in the string * @param endIndex the end index for the integer in the string * @throws NumberFormatException if the value is not a number */ private fun String.readInt(beginIndex: Int, endIndex: Int): Int { if (beginIndex < 0 || endIndex > length || beginIndex > endIndex) { throw NumberFormatException(this) } // use same logic as in Integer.parseInt() but less generic we're not supporting negative values var i = beginIndex var result = 0 var digit: Int if (i < endIndex) { digit = Character.digit(this[i++], 10) if (digit < 0) { throw NumberFormatException("Invalid number: " + this.substring(beginIndex, endIndex)) } result = -digit } while (i < endIndex) { digit = Character.digit(this[i++], 10) if (digit < 0) { throw NumberFormatException("Invalid number: " + this.substring(beginIndex, endIndex)) } result *= 10 result -= digit } return -result } /** * Zero pad a number to a specified length * * @param value the integer value to pad if necessary. * @param length the length of the string we should zero pad */ private fun StringBuilder.padInt(value: Int, length: Int) { val strValue = value.toString() for (i in length - strValue.length downTo 1) { append('0') } append(strValue) } /** * Returns the index of the first character in the string that is not a digit, starting at offset. */ private fun String.indexOfNonDigit(offset: Int): Int { for (i in offset until length) { val c = this[i] if (c !in '0'..'9') return i } return length } ================================================ FILE: moshi-adapters/src/main/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactory.kt ================================================ /* * Copyright (C) 2011 Google 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 * * https://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.squareup.moshi.adapters import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter.Factory import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonReader.Options import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.rawType import java.lang.reflect.Type import javax.annotation.CheckReturnValue import okio.IOException /** * A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON * Moshi uses this type information to determine which class to decode to. When encoding Moshi uses * the object’s class to determine what type information to include. * * Suppose we have an interface, its implementations, and a class that uses them: * ``` * interface HandOfCards { } * * class BlackjackHand implements HandOfCards { * Card hidden_card; * List visible_cards; * } * * class HoldemHand implements HandOfCards { * Set hidden_cards; * } * * class Player { * String name; * HandOfCards hand; * } * ``` * * We want to decode the following JSON into the player model above: * ``` * { * "name": "Jesse", * "hand": { * "hand_type": "blackjack", * "hidden_card": "9D", * "visible_cards": ["8H", "4C"] * } * } * ``` * * Left unconfigured, Moshi would incorrectly attempt to decode the hand object to the abstract * `HandOfCards` interface. We configure it to use the appropriate subtype instead: * ``` * Moshi moshi = new Moshi.Builder() * .add(PolymorphicJsonAdapterFactory.of(HandOfCards.class, "hand_type") * .withSubtype(BlackjackHand.class, "blackjack") * .withSubtype(HoldemHand.class, "holdem")) * .build(); * ``` * * This class imposes strict requirements on its use: * * Base types may be classes or interfaces. * * Subtypes must encode as JSON objects. * * Type information must be in the encoded object. Each message must have a type label like * `hand_type` whose value is a string like `blackjack` that identifies which type to use. * * Each type identifier must be unique. * * For best performance type information should be the first field in the object. Otherwise Moshi * must reprocess the JSON stream once it knows the object's type. * * If an unknown subtype label is encountered when decoding, or if the label key is missing * entirely: * * If [withDefaultValue] is used, then `defaultValue` will be returned. * * If [withFallbackJsonAdapter] is used, then the `fallbackJsonAdapter.fromJson(reader)` result * will be returned. * * Otherwise a [JsonDataException] will be thrown. * * If an unknown type is encountered when encoding: * * If [withFallbackJsonAdapter] is used, then the `fallbackJsonAdapter.toJson(writer, value)` * result will be returned. * * Otherwise a [IllegalArgumentException] will be thrown. * * If the same subtype has multiple labels the first one is used when encoding. */ public class PolymorphicJsonAdapterFactory internal constructor( private val baseType: Class, private val labelKey: String, private val labels: List, private val subtypes: List, private val fallbackJsonAdapter: JsonAdapter?, ) : Factory { /** Returns a new factory that decodes instances of `subtype`. */ public fun withSubtype(subtype: Class, label: String): PolymorphicJsonAdapterFactory { require(!labels.contains(label)) { "Labels must be unique." } val newLabels = buildList { addAll(labels) add(label) } val newSubtypes = buildList { addAll(subtypes) add(subtype) } return PolymorphicJsonAdapterFactory( baseType = baseType, labelKey = labelKey, labels = newLabels, subtypes = newSubtypes, fallbackJsonAdapter = fallbackJsonAdapter, ) } /** * Returns a new factory that with default to `fallbackJsonAdapter.fromJson(reader)` upon decoding * of unrecognized labels or when the label key is missing entirely. * * The [JsonReader] instance will not be automatically consumed, so make sure to consume it within * your implementation of [JsonAdapter.fromJson] */ public fun withFallbackJsonAdapter( fallbackJsonAdapter: JsonAdapter? ): PolymorphicJsonAdapterFactory { return PolymorphicJsonAdapterFactory( baseType = baseType, labelKey = labelKey, labels = labels, subtypes = subtypes, fallbackJsonAdapter = fallbackJsonAdapter, ) } /** * Returns a new factory that will default to `defaultValue` upon decoding of unrecognized labels * or when the label key is missing entirely. The default value should be immutable. */ public fun withDefaultValue(defaultValue: T?): PolymorphicJsonAdapterFactory { return withFallbackJsonAdapter(buildFallbackJsonAdapter(defaultValue)) } private fun buildFallbackJsonAdapter(defaultValue: T?): JsonAdapter { return object : JsonAdapter() { override fun fromJson(reader: JsonReader): Any? { reader.skipValue() return defaultValue } override fun toJson(writer: JsonWriter, value: Any?) { throw IllegalArgumentException( "Expected one of $subtypes but found $value, a ${value?.javaClass}. Register this subtype." ) } } } override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (type.rawType != baseType || annotations.isNotEmpty()) { return null } val jsonAdapters: List> = subtypes.map(moshi::adapter) return PolymorphicJsonAdapter(labelKey, labels, subtypes, jsonAdapters, fallbackJsonAdapter) .nullSafe() } internal class PolymorphicJsonAdapter( private val labelKey: String, private val labels: List, private val subtypes: List, private val jsonAdapters: List>, private val fallbackJsonAdapter: JsonAdapter?, ) : JsonAdapter() { /** Single-element options containing the label's key only. */ private val labelKeyOptions: Options = Options.of(labelKey) /** Corresponds to subtypes. */ private val labelOptions: Options = Options.of(*labels.toTypedArray()) override fun fromJson(reader: JsonReader): Any? { val peeked = reader.peekJson() peeked.failOnUnknown = false val labelIndex = peeked.use(::labelIndex) return if (labelIndex == -1) { fallbackJsonAdapter?.fromJson(reader) } else { jsonAdapters[labelIndex].fromJson(reader) } } private fun labelIndex(reader: JsonReader): Int { reader.beginObject() while (reader.hasNext()) { if (reader.selectName(labelKeyOptions) == -1) { reader.skipName() reader.skipValue() continue } val labelIndex = reader.selectString(labelOptions) if (labelIndex == -1 && fallbackJsonAdapter == null) { throw JsonDataException( "Expected one of $labels for key '$labelKey' but found '${reader.nextString()}'. Register a subtype for this label." ) } return labelIndex } if (fallbackJsonAdapter != null) { return -1 } throw JsonDataException("Missing label for $labelKey") } @Throws(IOException::class) override fun toJson(writer: JsonWriter, value: Any?) { val type: Class<*> = value!!.javaClass val labelIndex = subtypes.indexOf(type) val adapter: JsonAdapter = if (labelIndex == -1) { requireNotNull(fallbackJsonAdapter) { "Expected one of $subtypes but found $value, a ${value.javaClass}. Register this subtype." } } else { jsonAdapters[labelIndex] } writer.beginObject() if (adapter !== fallbackJsonAdapter) { writer.name(labelKey).value(labels[labelIndex]) } val flattenToken = writer.beginFlatten() adapter.toJson(writer, value) writer.endFlatten(flattenToken) writer.endObject() } override fun toString(): String { return "PolymorphicJsonAdapter($labelKey)" } } public companion object { /** * @param baseType The base type for which this factory will create adapters. Cannot be Object. * @param labelKey The key in the JSON object whose value determines the type to which to map * the JSON object. */ @JvmStatic @CheckReturnValue public fun of(baseType: Class, labelKey: String): PolymorphicJsonAdapterFactory { return PolymorphicJsonAdapterFactory( baseType = baseType, labelKey = labelKey, labels = emptyList(), subtypes = emptyList(), fallbackJsonAdapter = null, ) } } } ================================================ FILE: moshi-adapters/src/main/java/com/squareup/moshi/adapters/Rfc3339DateJsonAdapter.kt ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.adapters import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonReader.Token.NULL import com.squareup.moshi.JsonWriter import java.util.Date import okio.IOException /** * Formats dates using [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt), which is formatted like * `2015-09-26T18:23:50.250Z`. This adapter is null-safe. To use, add this as an adapter for * `Date.class` on your [Moshi.Builder][com.squareup.moshi.Moshi.Builder]: * ``` * Moshi moshi = new Moshi.Builder() * .add(Date.class, new Rfc3339DateJsonAdapter()) * .build(); * ``` */ public class Rfc3339DateJsonAdapter : JsonAdapter() { /** The underlying deserialization logic is thread-safe and does not require synchronization. * */ @Throws(IOException::class) override fun fromJson(reader: JsonReader): Date? { if (reader.peek() == NULL) { return reader.nextNull() } val string = reader.nextString() return string.parseIsoDate() } /** * The underlying serialization logic is thread-safe and does not require synchronization. * */ @Throws(IOException::class) override fun toJson(writer: JsonWriter, value: Date?) { if (value == null) { writer.nullValue() } else { val string = value.formatIsoDate() writer.value(string) } } } ================================================ FILE: moshi-adapters/src/test/java/com/squareup/moshi/adapters/EnumJsonAdapterTest.java ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.adapters; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import com.squareup.moshi.Json; import com.squareup.moshi.JsonDataException; import com.squareup.moshi.JsonReader; import okio.Buffer; import org.junit.Test; @SuppressWarnings("CheckReturnValue") public final class EnumJsonAdapterTest { @Test public void toAndFromJson() throws Exception { EnumJsonAdapter adapter = EnumJsonAdapter.create(Roshambo.class); assertThat(adapter.fromJson("\"ROCK\"")).isEqualTo(Roshambo.ROCK); assertThat(adapter.toJson(Roshambo.PAPER)).isEqualTo("\"PAPER\""); } @Test public void withJsonName() throws Exception { EnumJsonAdapter adapter = EnumJsonAdapter.create(Roshambo.class); assertThat(adapter.fromJson("\"scr\"")).isEqualTo(Roshambo.SCISSORS); assertThat(adapter.toJson(Roshambo.SCISSORS)).isEqualTo("\"scr\""); } @Test public void withoutFallbackValue() throws Exception { EnumJsonAdapter adapter = EnumJsonAdapter.create(Roshambo.class); JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\"")); try { adapter.fromJson(reader); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected one of [ROCK, PAPER, scr] but was SPOCK at path $"); } assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void withFallbackValue() throws Exception { EnumJsonAdapter adapter = EnumJsonAdapter.create(Roshambo.class).withUnknownFallback(Roshambo.ROCK); JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\"")); assertThat(adapter.fromJson(reader)).isEqualTo(Roshambo.ROCK); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void withNullFallbackValue() throws Exception { EnumJsonAdapter adapter = EnumJsonAdapter.create(Roshambo.class).withUnknownFallback(null); JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\"")); assertThat(adapter.fromJson(reader)).isNull(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } enum Roshambo { ROCK, PAPER, @Json(name = "scr") SCISSORS } } ================================================ FILE: moshi-adapters/src/test/java/com/squareup/moshi/adapters/PolymorphicJsonAdapterFactoryTest.java ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.adapters; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; 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 java.io.IOException; import java.util.Collections; import java.util.Map; import okio.Buffer; import org.jetbrains.annotations.Nullable; import org.junit.Test; @SuppressWarnings("CheckReturnValue") public final class PolymorphicJsonAdapterFactoryTest { @Test public void fromJson() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); assertThat(adapter.fromJson("{\"type\":\"success\",\"value\":\"Okay!\"}")) .isEqualTo(new Success("Okay!")); assertThat(adapter.fromJson("{\"type\":\"error\",\"error_logs\":{\"order\":66}}")) .isEqualTo(new Error(Collections.singletonMap("order", 66d))); } @Test public void toJson() { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); assertThat(adapter.toJson(new Success("Okay!"))) .isEqualTo("{\"type\":\"success\",\"value\":\"Okay!\"}"); assertThat(adapter.toJson(new Error(Collections.singletonMap("order", 66)))) .isEqualTo("{\"type\":\"error\",\"error_logs\":{\"order\":66}}"); } @Test public void unregisteredLabelValue() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); JsonReader reader = JsonReader.of(new Buffer().writeUtf8("{\"type\":\"data\",\"value\":\"Okay!\"}")); try { adapter.fromJson(reader); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Expected one of [success, error] for key 'type' but found" + " 'data'. Register a subtype for this label."); } assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); } @Test public void specifiedFallbackSubtype() throws IOException { Error fallbackError = new Error(Collections.emptyMap()); Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error") .withDefaultValue(fallbackError)) .build(); JsonAdapter adapter = moshi.adapter(Message.class); Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}"); assertThat(message).isSameInstanceAs(fallbackError); } @Test public void specifiedNullFallbackSubtype() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error") .withDefaultValue(null)) .build(); JsonAdapter adapter = moshi.adapter(Message.class); Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}"); assertThat(message).isNull(); } @Test public void specifiedFallbackJsonAdapter() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error") .withFallbackJsonAdapter( new JsonAdapter() { @Override public Object fromJson(JsonReader reader) throws IOException { reader.beginObject(); assertThat(reader.nextName()).isEqualTo("type"); assertThat(reader.nextString()).isEqualTo("data"); assertThat(reader.nextName()).isEqualTo("value"); assertThat(reader.nextString()).isEqualTo("Okay!"); reader.endObject(); return new EmptyMessage(); } @Override public void toJson(JsonWriter writer, @Nullable Object value) { throw new AssertionError(); } })) .build(); JsonAdapter adapter = moshi.adapter(Message.class); JsonReader reader = JsonReader.of(new Buffer().writeUtf8("{\"type\":\"data\",\"value\":\"Okay!\"}")); Message message = adapter.fromJson(reader); assertThat(message).isInstanceOf(EmptyMessage.class); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void unregisteredSubtype() { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); try { adapter.toJson(new EmptyMessage()); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Expected one of [class" + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Success, class" + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Error] but found" + " EmptyMessage, a class" + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$EmptyMessage. Register" + " this subtype."); } } @Test public void unregisteredSubtypeWithDefaultValue() { Error fallbackError = new Error(Collections.emptyMap()); Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error") .withDefaultValue(fallbackError)) .build(); JsonAdapter adapter = moshi.adapter(Message.class); try { adapter.toJson(new EmptyMessage()); } catch (IllegalArgumentException expected) { assertThat(expected) .hasMessageThat() .isEqualTo( "Expected one of [class" + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Success, class" + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Error] but found" + " EmptyMessage, a class" + " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$EmptyMessage. Register" + " this subtype."); } } @Test public void unregisteredSubtypeWithFallbackJsonAdapter() { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error") .withFallbackJsonAdapter( new JsonAdapter() { @Override public Object fromJson(JsonReader reader) { throw new RuntimeException( "Not implemented as not needed for the test"); } @Override public void toJson(JsonWriter writer, Object value) throws IOException { writer.name("type").value("injected by fallbackJsonAdapter"); } })) .build(); JsonAdapter adapter = moshi.adapter(Message.class); String json = adapter.toJson(new EmptyMessage()); assertThat(json).isEqualTo("{\"type\":\"injected by fallbackJsonAdapter\"}"); } @Test public void nonStringLabelValue() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); try { adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}"); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected a string but was BEGIN_OBJECT at path $.type"); } } @Test public void nonObjectDoesNotConsume() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"Failure\"")); try { adapter.fromJson(reader); fail(); } catch (JsonDataException expected) { assertThat(expected) .hasMessageThat() .isEqualTo("Expected BEGIN_OBJECT but was STRING at path $"); } assertThat(reader.nextString()).isEqualTo("Failure"); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void nonUniqueSubtypes() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Success.class, "data") .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); assertThat(adapter.fromJson("{\"type\":\"success\",\"value\":\"Okay!\"}")) .isEqualTo(new Success("Okay!")); assertThat(adapter.fromJson("{\"type\":\"data\",\"value\":\"Data!\"}")) .isEqualTo(new Success("Data!")); assertThat(adapter.fromJson("{\"type\":\"error\",\"error_logs\":{\"order\":66}}")) .isEqualTo(new Error(Collections.singletonMap("order", 66d))); assertThat(adapter.toJson(new Success("Data!"))) .isEqualTo("{\"type\":\"success\",\"value\":\"Data!\"}"); } @Test public void uniqueLabels() { PolymorphicJsonAdapterFactory factory = PolymorphicJsonAdapterFactory.of(Message.class, "type").withSubtype(Success.class, "data"); try { factory.withSubtype(Error.class, "data"); fail(); } catch (IllegalArgumentException expected) { assertThat(expected).hasMessageThat().isEqualTo("Labels must be unique."); } } @Test public void nullSafe() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); JsonReader reader = JsonReader.of(new Buffer().writeUtf8("null")); assertThat(adapter.fromJson(reader)).isNull(); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } /** * Longs that do not have an exact double representation are problematic for JSON. It is a bad * idea to use JSON for these values! But Moshi tries to retain long precision where possible. */ @Test public void unportableTypes() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(MessageWithUnportableTypes.class, "unportable")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); assertThat(adapter.toJson(new MessageWithUnportableTypes(9007199254740993L))) .isEqualTo("{\"type\":\"unportable\",\"long_value\":9007199254740993}"); MessageWithUnportableTypes decoded = (MessageWithUnportableTypes) adapter.fromJson("{\"type\":\"unportable\",\"long_value\":9007199254740993}"); assertThat(decoded.long_value).isEqualTo(9007199254740993L); } @Test public void failOnUnknownMissingTypeLabel() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(MessageWithType.class, "success")) .build(); JsonAdapter adapter = moshi.adapter(Message.class).failOnUnknown(); MessageWithType decoded = (MessageWithType) adapter.fromJson("{\"value\":\"Okay!\",\"type\":\"success\"}"); assertThat(decoded.value).isEqualTo("Okay!"); } @Test public void missingLabelKey() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error")) .build(); JsonAdapter adapter = moshi.adapter(Message.class); JsonReader reader = JsonReader.of(new Buffer().writeUtf8("{\"value\":\"Okay!\"}")); try { adapter.fromJson(reader); fail(); } catch (JsonDataException expected) { assertThat(expected).hasMessageThat().isEqualTo("Missing label for type"); } assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT); } @Test public void missingLabelKeyWithSpecifiedFallbackJsonAdapter() throws IOException { Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error") .withFallbackJsonAdapter( new JsonAdapter() { @Override public Object fromJson(JsonReader reader) throws IOException { reader.beginObject(); assertThat(reader.nextName()).isEqualTo("value"); assertThat(reader.nextString()).isEqualTo("Okay!"); reader.endObject(); return new EmptyMessage(); } @Override public void toJson(JsonWriter writer, @Nullable Object value) { throw new AssertionError(); } })) .build(); JsonAdapter adapter = moshi.adapter(Message.class); JsonReader reader = JsonReader.of(new Buffer().writeUtf8("{\"value\":\"Okay!\"}")); Message message = adapter.fromJson(reader); assertThat(message).isInstanceOf(EmptyMessage.class); assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT); } @Test public void missingLabelKeyWithSpecifiedDefaultValue() throws IOException { Error fallbackError = new Error(Collections.emptyMap()); Moshi moshi = new Moshi.Builder() .add( PolymorphicJsonAdapterFactory.of(Message.class, "type") .withSubtype(Success.class, "success") .withSubtype(Error.class, "error") .withDefaultValue(fallbackError)) .build(); JsonAdapter adapter = moshi.adapter(Message.class); Message message = adapter.fromJson("{\"value\":\"Okay!\"}"); assertThat(message).isSameInstanceAs(fallbackError); } interface Message {} static final class Success implements Message { final String value; Success(String value) { this.value = value; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Success)) return false; Success success = (Success) o; return value.equals(success.value); } @Override public int hashCode() { return value.hashCode(); } } static final class Error implements Message { final Map error_logs; Error(Map error_logs) { this.error_logs = error_logs; } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Error)) return false; Error error = (Error) o; return error_logs.equals(error.error_logs); } @Override public int hashCode() { return error_logs.hashCode(); } } static final class EmptyMessage implements Message { @Override public String toString() { return "EmptyMessage"; } } static final class MessageWithUnportableTypes implements Message { final long long_value; MessageWithUnportableTypes(long long_value) { this.long_value = long_value; } } static final class MessageWithType implements Message { final String type; final String value; MessageWithType(String type, String value) { this.type = type; this.value = value; } } } ================================================ FILE: moshi-adapters/src/test/java/com/squareup/moshi/adapters/Rfc3339DateJsonAdapterTest.java ================================================ /* * Copyright (C) 2015 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 * * https://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.squareup.moshi.adapters; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.fail; import com.squareup.moshi.JsonAdapter; import com.squareup.moshi.JsonDataException; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; import java.util.concurrent.TimeUnit; import org.junit.Test; public final class Rfc3339DateJsonAdapterTest { private final JsonAdapter adapter = new Rfc3339DateJsonAdapter().lenient(); @Test public void fromJsonWithTwoDigitMillis() throws Exception { assertThat(adapter.fromJson("\"1985-04-12T23:20:50.52Z\"")) .isEqualTo(newDate(1985, 4, 12, 23, 20, 50, 520, 0)); } @Test public void fromJson() throws Exception { assertThat(adapter.fromJson("\"1970-01-01T00:00:00.000Z\"")) .isEqualTo(newDate(1970, 1, 1, 0, 0, 0, 0, 0)); assertThat(adapter.fromJson("\"1985-04-12T23:20:50.520Z\"")) .isEqualTo(newDate(1985, 4, 12, 23, 20, 50, 520, 0)); assertThat(adapter.fromJson("\"1996-12-19T16:39:57-08:00\"")) .isEqualTo(newDate(1996, 12, 19, 16, 39, 57, 0, -8 * 60)); assertThat(adapter.fromJson("\"1990-12-31T23:59:60Z\"")) .isEqualTo(newDate(1990, 12, 31, 23, 59, 59, 0, 0)); assertThat(adapter.fromJson("\"1990-12-31T15:59:60-08:00\"")) .isEqualTo(newDate(1990, 12, 31, 15, 59, 59, 0, -8 * 60)); assertThat(adapter.fromJson("\"1937-01-01T12:00:27.870+00:20\"")) .isEqualTo(newDate(1937, 1, 1, 12, 0, 27, 870, 20)); } @Test public void toJson() throws Exception { assertThat(adapter.toJson(newDate(1970, 1, 1, 0, 0, 0, 0, 0))) .isEqualTo("\"1970-01-01T00:00:00.000Z\""); assertThat(adapter.toJson(newDate(1985, 4, 12, 23, 20, 50, 520, 0))) .isEqualTo("\"1985-04-12T23:20:50.520Z\""); assertThat(adapter.toJson(newDate(1996, 12, 19, 16, 39, 57, 0, -8 * 60))) .isEqualTo("\"1996-12-20T00:39:57.000Z\""); assertThat(adapter.toJson(newDate(1990, 12, 31, 23, 59, 59, 0, 0))) .isEqualTo("\"1990-12-31T23:59:59.000Z\""); assertThat(adapter.toJson(newDate(1990, 12, 31, 15, 59, 59, 0, -8 * 60))) .isEqualTo("\"1990-12-31T23:59:59.000Z\""); assertThat(adapter.toJson(newDate(1937, 1, 1, 12, 0, 27, 870, 20))) .isEqualTo("\"1937-01-01T11:40:27.870Z\""); } @Test public void nullSafety() throws Exception { assertThat(adapter.toJson(null)).isEqualTo("null"); assertThat(adapter.fromJson("null")).isNull(); } @Test public void presentOrAbsentTime() throws Exception { assertThat(adapter.fromJson("\"1970-01-01T12:34:56.789Z\"")) .isEqualTo(newDate(1970, 1, 1, 12, 34, 56, 789, 0)); assertThat(adapter.fromJson("\"1970-01-01Z\"")).isEqualTo(newDate(1970, 1, 1, 0, 0, 0, 0, 0)); assertThat(adapter.fromJson("\"1970-01-01-08:00\"")) .isEqualTo(newDate(1970, 1, 1, 0, 0, 0, 0, -8 * 60)); } @Test public void variableFractionDigits() throws Exception { assertThat(adapter.fromJson("\"1970-01-01T00:00:00.1Z\"")) .isEqualTo(newDate(1970, 1, 1, 0, 0, 0, 100, 0)); assertThat(adapter.fromJson("\"1970-01-01T00:00:00.12Z\"")) .isEqualTo(newDate(1970, 1, 1, 0, 0, 0, 120, 0)); assertThat(adapter.fromJson("\"1970-01-01T00:00:00.123Z\"")) .isEqualTo(newDate(1970, 1, 1, 0, 0, 0, 123, 0)); } @Test public void absentTimeZone() throws Exception { assertThat(adapter.fromJson("\"1970-01-01\"")).isEqualTo(newDateWithHostZone(1970, 1, 1)); assertThat(adapter.fromJson("\"1970-01-01Z\"")).isEqualTo(newDate(1970, 1, 1, 0, 0, 0, 0, 0)); try { adapter.fromJson("\"1970-01-01T00:00:00.000\""); fail(); } catch (JsonDataException expected) { } } private Date newDate( int year, int month, int day, int hour, int minute, int second, int millis, int offset) { Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT")); calendar.set(year, month - 1, day, hour, minute, second); calendar.set(Calendar.MILLISECOND, millis); return new Date(calendar.getTimeInMillis() - TimeUnit.MINUTES.toMillis(offset)); } /** * Dates specified without any time or timezone (like "1970-01-01") are returned in the host * computer's time zone. This is a longstanding bug that we're attempting to stay consistent with. */ private Date newDateWithHostZone(int year, int month, int day) { Calendar calendar = new GregorianCalendar(); calendar.set(year, month - 1, day, 0, 0, 0); calendar.set(Calendar.MILLISECOND, 0); return new Date(calendar.getTimeInMillis()); } } ================================================ FILE: moshi-kotlin/build.gradle.kts ================================================ import org.gradle.jvm.tasks.Jar plugins { kotlin("jvm") id("com.vanniktech.maven.publish") id("org.jetbrains.dokka") } dependencies { api(project(":moshi")) implementation(libs.kotlin.metadata) testImplementation(kotlin("test")) testImplementation(libs.junit) testImplementation(libs.truth) } tasks.withType().configureEach { manifest { attributes("Automatic-Module-Name" to "com.squareup.moshi.kotlin") } } ================================================ FILE: moshi-kotlin/src/main/java/com/squareup/moshi/KotlinJsonAdapterFactory.kt ================================================ /* * 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 * * https://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.squareup.moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory @Deprecated( message = "this moved to avoid a package name conflict in the Java Platform Module System.", replaceWith = ReplaceWith("com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory"), ) public class KotlinJsonAdapterFactory : JsonAdapter.Factory by KotlinJsonAdapterFactory() ================================================ FILE: moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/IndexedParameterMap.kt ================================================ package com.squareup.moshi.kotlin.reflect /** A simple [Map] that uses parameter indexes instead of sorting or hashing. */ internal class IndexedParameterMap( private val parameterKeys: List, private val parameterValues: Array, ) : AbstractMutableMap() { override fun put(key: KtParameter, value: Any?): Any? = null override val entries: MutableSet> get() { val allPossibleEntries = parameterKeys.mapIndexed { index, value -> SimpleEntry(value, parameterValues[index]) } return allPossibleEntries.filterTo(mutableSetOf()) { it.value !== ABSENT_VALUE } } override fun containsKey(key: KtParameter) = parameterValues[key.index] !== ABSENT_VALUE override fun get(key: KtParameter): Any? { val value = parameterValues[key.index] return if (value !== ABSENT_VALUE) value else null } } ================================================ FILE: moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/Invokable.kt ================================================ package com.squareup.moshi.kotlin.reflect import com.squareup.moshi.internal.DEFAULT_CONSTRUCTOR_MARKER import java.lang.reflect.Constructor import java.lang.reflect.Method private val DEFAULT_CONSTRUCTOR_SIGNATURE by lazy(LazyThreadSafetyMode.NONE) { DEFAULT_CONSTRUCTOR_MARKER!!.descriptor } /** * A thin wrapper over [Constructor] and [Method] to avoid using [java.lang.reflect.Executable], * which is not available on Android until API 26. */ internal sealed interface Invokable { val parameterTypes: Array> val parameterAnnotations: Array> val jvmMethodSignature: String fun setAccessible() fun defaultsSignature(): String @JvmInline value class InvokableConstructor(val constructor: Constructor<*>) : Invokable { override val parameterTypes: Array> get() = constructor.parameterTypes override val parameterAnnotations: Array> get() = constructor.parameterAnnotations override val jvmMethodSignature: String get() = constructor.jvmMethodSignature override fun setAccessible() { constructor.isAccessible = true } override fun defaultsSignature(): String { val rawPrefix = jvmMethodSignature.removeSuffix("V").removeSuffix(")") return buildDefaultsSignature(rawPrefix, parameterTypes.size, "V") } } @JvmInline value class InvokableMethod(val method: Method) : Invokable { override val parameterTypes: Array> get() = method.parameterTypes override val parameterAnnotations: Array> get() = method.parameterAnnotations override val jvmMethodSignature: String get() = method.jvmMethodSignature override fun setAccessible() { method.isAccessible = true } override fun defaultsSignature(): String { val suffixDescriptor = method.returnType.descriptor val rawPrefix = jvmMethodSignature.removeSuffix(suffixDescriptor).removeSuffix(")") // Need to add $default to the end of the method name val (name, rest) = rawPrefix.split("(", limit = 2) // ktlint doesn't support multi-dollar prefixes @Suppress("CanConvertToMultiDollarString") val prefix = "$name\$default($rest" return buildDefaultsSignature(prefix, parameterTypes.size, suffixDescriptor) } } } private fun buildDefaultsSignature(prefix: String, parameterCount: Int, suffix: String): String { val maskParamsToAdd = (parameterCount + 31) / 32 return buildString { append(prefix) repeat(maskParamsToAdd) { append("I") } append(DEFAULT_CONSTRUCTOR_SIGNATURE) append(')') append(suffix) } } ================================================ FILE: moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/JvmDescriptors.kt ================================================ /* * Copyright (C) 2025 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 * * https://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.squareup.moshi.kotlin.reflect import java.lang.reflect.Constructor import java.lang.reflect.Method import kotlin.metadata.jvm.JvmMethodSignature private val PRIMITIVE_CLASS_TO_SYMBOL = buildMap(capacity = 9) { put(Byte::class.javaPrimitiveType, 'B') put(Char::class.javaPrimitiveType, 'C') put(Double::class.javaPrimitiveType, 'D') put(Float::class.javaPrimitiveType, 'F') put(Int::class.javaPrimitiveType, 'I') put(Long::class.javaPrimitiveType, 'J') put(Short::class.javaPrimitiveType, 'S') put(Boolean::class.javaPrimitiveType, 'Z') put(Void::class.javaPrimitiveType, 'V') } internal val Class<*>.descriptor: String get() { return when { isPrimitive -> PRIMITIVE_CLASS_TO_SYMBOL[this]?.toString() ?: throw RuntimeException("Unrecognized primitive $this") isArray -> "[${componentType.descriptor}" else -> "L$name;".replace('.', '/') } } private class TypeSignatureReader(var index: Int) { fun readType(desc: String): Class<*> { return when (val c = desc[index]) { '[' -> { // It's an array index++ val start = index this.readType(desc) // We ignore the read class because we just want to reuse the descriptor name in the string // since that's how array component lookups work val descriptorName = buildString { for (i in start until index) { var subc = desc[i] if (subc == '/') { subc = '.' } this.append(subc) } } Class.forName("[$descriptorName") } 'B' -> { index++ Byte::class.javaPrimitiveType!! } 'C' -> { index++ Char::class.javaPrimitiveType!! } 'D' -> { index++ Double::class.javaPrimitiveType!! } 'F' -> { index++ Float::class.javaPrimitiveType!! } 'I' -> { index++ Int::class.javaPrimitiveType!! } 'J' -> { index++ Long::class.javaPrimitiveType!! } 'S' -> { index++ Short::class.javaPrimitiveType!! } 'Z' -> { index++ Boolean::class.javaPrimitiveType!! } 'V' -> { index++ Void::class.javaPrimitiveType!! } 'L' -> { // It's a ClassName, read it until ';' index++ var c2 = desc[index] val className = buildString { while (c2 != ';') { if (c2 == '/') { // convert package splits to '.' c2 = '.' } this.append(c2) index++ c2 = desc[index] } } index++ // Read off the ';' Class.forName(className) } else -> error("Unknown character $c") } } } internal fun JvmMethodSignature.decodeParameterTypes(): List> { val classList = mutableListOf>() val typeSignatureReader = TypeSignatureReader(0) while (typeSignatureReader.index < descriptor.length) { when (descriptor[typeSignatureReader.index]) { '(' -> { typeSignatureReader.index++ continue } ')' -> break } classList += typeSignatureReader.readType(descriptor) } return classList } /** * Returns the JVM signature in the form "name$MethodDescriptor", for example: * `"(Ljava/lang/Object;)V"` or `"foo(Ljava/lang/Object;)V"`. * * Useful for comparing with [JvmMethodSignature]. * * @see JVM * specification, section 4.3 */ private fun jvmMethodSignature( name: String, parameterTypes: Array>, returnDescriptor: String, ): String = buildString { append(name) parameterTypes.joinTo(buffer = this, separator = "", prefix = "(", postfix = ")") { it.descriptor } append(returnDescriptor) } internal val Constructor<*>.jvmMethodSignature: String get() = jvmMethodSignature("", parameterTypes, "V") internal val Method.jvmMethodSignature: String get() = jvmMethodSignature(name, parameterTypes, returnType.descriptor) ================================================ FILE: moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/JvmSignatureSearcher.kt ================================================ package com.squareup.moshi.kotlin.reflect import java.lang.reflect.Field import java.lang.reflect.Method import kotlin.metadata.KmProperty import kotlin.metadata.jvm.JvmFieldSignature import kotlin.metadata.jvm.JvmMethodSignature import kotlin.metadata.jvm.fieldSignature import kotlin.metadata.jvm.getterSignature import kotlin.metadata.jvm.setterSignature import kotlin.metadata.jvm.syntheticMethodForAnnotations internal class JvmSignatureSearcher(private val clazz: Class<*>) { fun syntheticMethodForAnnotations(kmProperty: KmProperty): Method? = kmProperty.syntheticMethodForAnnotations?.let { signature -> findMethod(clazz, signature) } fun getter(kmProperty: KmProperty): Method? = kmProperty.getterSignature?.let { signature -> findMethod(clazz, signature) } fun setter(kmProperty: KmProperty): Method? = kmProperty.setterSignature?.let { signature -> findMethod(clazz, signature) } fun field(kmProperty: KmProperty): Field? = kmProperty.fieldSignature?.let { signature -> findField(clazz, signature) } private fun findMethod(sourceClass: Class<*>, signature: JvmMethodSignature): Method { val parameterTypes = signature.decodeParameterTypes() return try { if (parameterTypes.isEmpty()) { // Save the empty copy sourceClass.getDeclaredMethod(signature.name) } else { sourceClass.getDeclaredMethod(signature.name, *parameterTypes.toTypedArray()) } } catch (e: NoSuchMethodException) { // Try finding the superclass method val superClass = sourceClass.superclass if (superClass != Any::class.java) { return findMethod(superClass, signature) } else { throw e } } } private fun findField(sourceClass: Class<*>, signature: JvmFieldSignature): Field { return try { sourceClass.getDeclaredField(signature.name) } catch (e: NoSuchFieldException) { // Try finding the superclass field val superClass = sourceClass.superclass if (superClass != Any::class.java) { return findField(superClass, signature) } else { throw e } } } } ================================================ FILE: moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KmExecutable.kt ================================================ package com.squareup.moshi.kotlin.reflect import java.lang.reflect.Constructor import java.lang.reflect.Method import java.lang.reflect.Modifier import kotlin.metadata.KmClass import kotlin.metadata.KmClassifier import kotlin.metadata.isSecondary import kotlin.metadata.isValue import kotlin.metadata.jvm.KotlinClassMetadata import kotlin.metadata.jvm.signature /** * Simple facade over KM constructor-ish types, which could be a constructor or a static creator * method (value classes). */ internal sealed class KmExecutable { abstract val parameters: List abstract val isDefault: Boolean abstract val needsDefaultMarker: Boolean companion object { /** * Checks if a type (from KM metadata) is a value class and returns its box-impl and unbox-impl * methods if so. * * Returns two nulls if this isn't a value class. */ internal fun findValueClassMethods( classifier: KmClassifier, classLoader: ClassLoader, ): Pair { if (classifier !is KmClassifier.Class) { // Not a class type (could be type parameter, etc.) return null to null } // Convert kotlin metadata class name to Java class name val className = classifier.name.replace('/', '.') val clazz = try { classLoader.loadClass(className) } catch (_: ClassNotFoundException) { return null to null } // Check if it's a value class using Kotlin metadata val metadata = clazz.getAnnotation(Metadata::class.java) ?: return null to null val classMetadata = KotlinClassMetadata.readLenient(metadata) if (classMetadata !is KotlinClassMetadata.Class) { return null to null } val kmClass = classMetadata.kmClass if (!kmClass.isValue) { return null to null } // Find the box-impl and unbox-impl methods val boxImplMethod = clazz.declaredMethods.firstOrNull { it.name == "box-impl" } ?: return null to null val unboxImplMethod = clazz.declaredMethods.firstOrNull { it.name == "unbox-impl" } ?: return null to null boxImplMethod.isAccessible = true unboxImplMethod.isAccessible = true return boxImplMethod to unboxImplMethod } operator fun invoke(rawType: Class<*>, kmClass: KmClass): KmExecutable<*>? { // If this is a value class, the "constructor" will actually be a static creator function val constructorsBySignature: Map = if (kmClass.isValue) { // kmConstructorSignature is something like constructor-impl(I)I // Need to look up the matching static function rawType.declaredMethods .filter { Modifier.isStatic(it.modifiers) } .associateBy({ it.jvmMethodSignature }, { Invokable.InvokableMethod(it) }) } else { rawType.declaredConstructors.associateBy( { it.jvmMethodSignature }, { Invokable.InvokableConstructor(it) }, ) } val kmConstructor = kmClass.constructors.find { !it.isSecondary } ?: return null val kmConstructorSignature = kmConstructor.signature?.toString() ?: return null val jvmConstructor = constructorsBySignature[kmConstructorSignature] ?: return null val parameters = kmConstructor.valueParameters.withIndex().map { (index, kmParam) -> // Check if this parameter's type is a value class val (parameterValueClassBoxer, parameterValueClassUnboxer) = findValueClassMethods(kmParam.type.classifier, rawType.classLoader) KtParameter( kmParam, index, jvmConstructor.parameterTypes[index], jvmConstructor.parameterAnnotations[index].toList(), parameterValueClassBoxer, parameterValueClassUnboxer, ) } val anyOptional = parameters.any { it.declaresDefaultValue } val actualConstructor = if (anyOptional) { constructorsBySignature[jvmConstructor.defaultsSignature()] ?: return null } else { jvmConstructor } actualConstructor.setAccessible() return if (kmClass.isValue) { // Things get quirky here. KM will return the primary constructor for the value class // as a constructor-impl static function, BUT this function always only returns the // underlying type. What we want is the boxed type, so we need to be able to invoke the // constructor and then be able to pass it on to the box-impl function to get the full // instance. We can't just skip ahead and use only the box-impl function because the // constructor is the only one that handles default values val boxImpl = constructorsBySignature.entries.first { it.key.startsWith("box-impl") }.value as Invokable.InvokableMethod KmExecutableFunction( (actualConstructor as Invokable.InvokableMethod).method, parameters, anyOptional, boxImpl.method, ) } else { KmExecutableConstructor( (actualConstructor as Invokable.InvokableConstructor).constructor, parameters, anyOptional, ) } } } private fun prepareFinalArgs(arguments: Array, masks: List): Array { if (!needsDefaultMarker) return arguments val allArgs = ArrayList(arguments.size + masks.size + 1) allArgs.addAll(arguments) if (isDefault) { allArgs.addAll(masks) } allArgs.add(null) // DefaultConstructorMarker return allArgs.toTypedArray() } @Suppress("UNCHECKED_CAST") fun newInstance(arguments: Array, masks: List): T { return when (this) { is KmExecutableConstructor -> { val finalArgs = prepareFinalArgs(arguments, masks) defaultsExecutable.newInstance(*finalArgs) } is KmExecutableFunction -> { val finalArgs = prepareFinalArgs(arguments, masks) // First get the instance returned by the constructor-impl val instance = defaultsExecutable.invoke(null, *finalArgs) // Then box it boxImpl.invoke(null, instance) } } as T } class KmExecutableConstructor( val defaultsExecutable: Constructor<*>, override val parameters: List, override val isDefault: Boolean, ) : KmExecutable>() { override val needsDefaultMarker = isDefault || parameters.any { it.isValueClass } } class KmExecutableFunction( val defaultsExecutable: Method, override val parameters: List, override val isDefault: Boolean, val boxImpl: Method, ) : KmExecutable() { override val needsDefaultMarker = isDefault || parameters.any { it.isValueClass } } } ================================================ FILE: moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterFactory.kt ================================================ /* * 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 * * https://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.squareup.moshi.kotlin.reflect import com.squareup.moshi.Json import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.internal.generatedAdapter import com.squareup.moshi.internal.isPlatformType import com.squareup.moshi.internal.jsonAnnotations import com.squareup.moshi.internal.missingProperty import com.squareup.moshi.internal.resolve import com.squareup.moshi.internal.unexpectedNull import com.squareup.moshi.rawType import java.lang.reflect.Modifier import java.lang.reflect.Type import kotlin.metadata.ClassKind import kotlin.metadata.KmClass import kotlin.metadata.KmFlexibleTypeUpperBound import kotlin.metadata.KmType import kotlin.metadata.KmTypeProjection import kotlin.metadata.Modality import kotlin.metadata.Visibility import kotlin.metadata.isInner import kotlin.metadata.isNullable import kotlin.metadata.isVar import kotlin.metadata.jvm.KotlinClassMetadata import kotlin.metadata.jvm.Metadata import kotlin.metadata.kind import kotlin.metadata.modality import kotlin.metadata.visibility /** Classes annotated with this are eligible for this adapter. */ private val KOTLIN_METADATA = Metadata::class.java /** * Placeholder value used when a field is absent from the JSON. Note that this code distinguishes * between absent values and present-but-null values. */ internal val ABSENT_VALUE = Any() /** * This class encodes Kotlin classes using their properties. It decodes them by first invoking the * constructor, and then by setting any additional properties that exist, if any. */ internal class KotlinJsonAdapter( private val constructor: KtConstructor, private val allBindings: List?>, private val nonIgnoredBindings: List>, private val options: JsonReader.Options, ) : JsonAdapter() { override fun fromJson(reader: JsonReader): T { val constructorSize = constructor.parameters.size // Read each value into its slot in the array. val values = Array(allBindings.size) { ABSENT_VALUE } reader.beginObject() while (reader.hasNext()) { val index = reader.selectName(options) if (index == -1) { reader.skipName() reader.skipValue() continue } val binding = nonIgnoredBindings[index] val propertyIndex = binding.propertyIndex if (values[propertyIndex] !== ABSENT_VALUE) { throw JsonDataException("Multiple values for '${binding.property.name}' at ${reader.path}") } values[propertyIndex] = binding.adapter.fromJson(reader) if (values[propertyIndex] == null && !binding.property.km.returnType.isNullable) { throw unexpectedNull(binding.property.name, binding.jsonName, reader) } } reader.endObject() // Confirm all parameters are present, optional, or nullable. for (i in 0 until constructorSize) { if (values[i] === ABSENT_VALUE) { val param = constructor.parameters[i] if (!param.declaresDefaultValue) { if (!param.isNullable) { throw missingProperty(constructor.parameters[i].name, allBindings[i]?.jsonName, reader) } values[i] = null // Replace absent with null. } } } // Call the constructor using a Map so that absent optionals get defaults. val result = constructor.callBy(IndexedParameterMap(constructor.parameters, values)) // Set remaining properties. for (i in constructorSize until allBindings.size) { val binding = allBindings[i]!! val value = values[i] binding.set(result, value) } return result } override fun toJson(writer: JsonWriter, value: T?) { if (value == null) throw NullPointerException("value == null") writer.beginObject() for (binding in allBindings) { if (binding == null) continue // Skip constructor parameters that aren't properties. writer.name(binding.name) binding.adapter.toJson(writer, binding.get(value)) } writer.endObject() } override fun toString() = "KotlinJsonAdapter(${constructor.type.canonicalName})" data class Binding( val name: String, val jsonName: String, val adapter: JsonAdapter

, val property: KtProperty, val propertyIndex: Int = property.parameter?.index ?: -1, ) { fun get(value: K): Any? { val rawValue = if (property.jvmGetter != null) { property.jvmGetter.invoke(value) } else if (property.jvmField != null) { property.jvmField.get(value) } else { error("Could not get JVM field or invoke JVM getter for property '$name'") } // If this property is a value class, box the raw value return if (property.isValueClass && rawValue != null) { property.valueClassBoxer!!.invoke(null, rawValue) } else { rawValue } } fun set(result: K, value: P) { if (value !== ABSENT_VALUE) { // If this property is a value class, unbox the value before setting val actualValue = if (property.isValueClass && value != null) { property.valueClassUnboxer!!.invoke(value) } else { value } val setter = property.jvmSetter if (setter != null) { setter.invoke(result, actualValue) return } property.jvmField?.set(result, actualValue) } } } } /** * A JsonAdapter for inline types that reads/writes the single property value directly without * wrapping it in a JSON object. */ private class InlineKotlinJsonAdapter( private val constructor: KtConstructor, private val binding: KotlinJsonAdapter.Binding, ) : JsonAdapter() { override fun fromJson(reader: JsonReader): T { // Read the value directly val value = binding.adapter.fromJson(reader) if (value == null && !binding.property.km.returnType.isNullable) { throw unexpectedNull(binding.property.name, binding.jsonName, reader) } @Suppress("UNCHECKED_CAST") return constructor.callBy(IndexedParameterMap(constructor.parameters, arrayOf(value))) } override fun toJson(writer: JsonWriter, value: T?) { if (value == null) throw NullPointerException("value == null") binding.adapter.toJson(writer, binding.get(value)) } override fun toString() = "InlineKotlinJsonAdapter(${constructor.type.canonicalName})" } public class KotlinJsonAdapterFactory : JsonAdapter.Factory { override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { if (annotations.isNotEmpty()) return null val rawType = type.rawType if (rawType.isInterface) return null if (rawType.isEnum) return null if (!rawType.isAnnotationPresent(KOTLIN_METADATA)) return null if (rawType.isPlatformType) return null try { val generatedAdapter = moshi.generatedAdapter(type, rawType) if (generatedAdapter != null) { return generatedAdapter } } catch (e: RuntimeException) { if (e.cause !is ClassNotFoundException) { throw e } // Fall back to a reflective adapter when the generated adapter is not found. } require(!rawType.isLocalClass) { "Cannot serialize local class or object expression ${rawType.name}" } require(!rawType.isAnonymousClass) { "Cannot serialize anonymous class ${rawType.name}" } val kmClass = rawType.header()?.toKmClass() ?: return null require(kmClass.modality != Modality.ABSTRACT) { "Cannot serialize abstract class ${rawType.name}" } require(!kmClass.isInner) { "Cannot serialize inner class ${rawType.name}" } require(kmClass.kind != ClassKind.OBJECT) { "Cannot serialize object declaration ${rawType.name}" } require(kmClass.kind != ClassKind.COMPANION_OBJECT) { "Cannot serialize companion object declaration ${rawType.name}" } require(kmClass.modality != Modality.SEALED) { "Cannot reflectively serialize sealed class ${rawType.name}. Please register an adapter." } val ktConstructor = KtConstructor.primary(rawType, kmClass) ?: return null val allPropertiesSequence = kmClass.properties.asSequence() + generateSequence(rawType) { it.superclass } .mapNotNull { it.header()?.toKmClass() } .flatMap { it.properties.asSequence() } .filterNot { it.visibility == Visibility.PRIVATE || it.visibility == Visibility.PRIVATE_TO_THIS } .filter { it.isVar } val signatureSearcher = JvmSignatureSearcher(rawType) val bindingsByName = LinkedHashMap>() val parametersByName = ktConstructor.parameters.associateBy { it.name } for (property in allPropertiesSequence.distinctBy { it.name }) { val propertyField = signatureSearcher.field(property) val ktParameter = parametersByName[property.name] if (ktParameter != null) { require(ktParameter.km.type valueEquals property.returnType) { "'${property.name}' has a constructor parameter of type ${ktParameter.km.type.canonicalName} but a property of type ${property.returnType.canonicalName}." } } if (!property.isVar && ktParameter == null) continue val getterMethod = signatureSearcher.getter(property) val setterMethod = signatureSearcher.setter(property) val annotationsMethod = signatureSearcher.syntheticMethodForAnnotations(property) // Check if the property's return type is a value class val (propertyValueClassBoxer, propertyValueClassUnboxer) = KmExecutable.findValueClassMethods(property.returnType.classifier, rawType.classLoader) val ktProperty = KtProperty( km = property, jvmField = propertyField, jvmGetter = getterMethod, jvmSetter = setterMethod, jvmAnnotationsMethod = annotationsMethod, parameter = ktParameter, valueClassBoxer = propertyValueClassBoxer, valueClassUnboxer = propertyValueClassUnboxer, ) val allAnnotations = ktProperty.annotations.toMutableList() val jsonAnnotation = allAnnotations.filterIsInstance().firstOrNull() val isIgnored = Modifier.isTransient(propertyField?.modifiers ?: 0) || jsonAnnotation?.ignore == true if (isIgnored) { ktParameter?.run { require(declaresDefaultValue) { "No default value for transient/ignored constructor parameter '$name' on type '${rawType.canonicalName}'" } } continue } val name = jsonAnnotation?.name ?: property.name val resolvedPropertyType = ktProperty.javaType.resolve(type, rawType) val adapter = moshi.adapter( resolvedPropertyType, allAnnotations.toTypedArray().jsonAnnotations, property.name, ) bindingsByName[property.name] = KotlinJsonAdapter.Binding(name, jsonAnnotation?.name ?: name, adapter, ktProperty) } val bindings = ArrayList?>() for (parameter in ktConstructor.parameters) { val binding = bindingsByName.remove(parameter.name) require(binding != null || parameter.declaresDefaultValue) { "No property for required constructor parameter '${parameter.name}' on type '${rawType.canonicalName}'" } bindings += binding } var index = bindings.size for (bindingByName in bindingsByName) { bindings += bindingByName.value.copy(propertyIndex = index++) } val nonIgnoredBindings = bindings.filterNotNull() // Check if this is an inline type val jsonClassAnnotation = rawType.getAnnotation(JsonClass::class.java) if (jsonClassAnnotation?.inline == true) { require(nonIgnoredBindings.size == 1) { "@JsonClass with inline = true requires exactly one non-transient property, " + "but ${rawType.canonicalName} has ${nonIgnoredBindings.size}: " + "${nonIgnoredBindings.joinToString { it.name }}." } val inlineBinding = nonIgnoredBindings[0] require(!inlineBinding.property.km.returnType.isNullable) { "@JsonClass with inline = true requires a non-nullable property, " + "but ${rawType.canonicalName}.${inlineBinding.name} is nullable." } @Suppress("UNCHECKED_CAST") return InlineKotlinJsonAdapter(ktConstructor, inlineBinding).nullSafe() } val options = JsonReader.Options.of(*nonIgnoredBindings.map { it.name }.toTypedArray()) return KotlinJsonAdapter(ktConstructor, bindings, nonIgnoredBindings, options).nullSafe() } private infix fun KmType?.valueEquals(other: KmType?): Boolean { return when { this === other -> true this != null && other != null -> { // Note we don't check abbreviatedType because typealiases and their backing types are equal // for our purposes. arguments valueEquals other.arguments && classifier == other.classifier && isNullable == other.isNullable && flexibleTypeUpperBound valueEquals other.flexibleTypeUpperBound && outerType valueEquals other.outerType } else -> false } } private infix fun List.valueEquals(other: List): Boolean { // check collections aren't same if (this !== other) { // fast check of sizes if (this.size != other.size) return false // check this and other contains same elements at position for (i in indices) { if (!(get(i) valueEquals other[i])) { return false } } } // collections are same or they contain same elements with same order return true } private infix fun KmTypeProjection?.valueEquals(other: KmTypeProjection?): Boolean { return when { this === other -> true this != null && other != null -> { variance == other.variance && type valueEquals other.type } else -> false } } private infix fun KmFlexibleTypeUpperBound?.valueEquals( other: KmFlexibleTypeUpperBound? ): Boolean { return when { this === other -> true this != null && other != null -> { typeFlexibilityId == other.typeFlexibilityId && type valueEquals other.type } else -> false } } private fun Class<*>.header(): Metadata? { val metadata = getAnnotation(KOTLIN_METADATA) ?: return null return with(metadata) { Metadata( kind = kind, metadataVersion = metadataVersion, data1 = data1, data2 = data2, extraString = extraString, packageName = packageName, extraInt = extraInt, ) } } private fun Metadata.toKmClass(): KmClass? { val classMetadata = KotlinClassMetadata.readLenient(this) if (classMetadata !is KotlinClassMetadata.Class) { return null } return classMetadata.kmClass } } ================================================ FILE: moshi-kotlin/src/main/java/com/squareup/moshi/kotlin/reflect/KtTypes.kt ================================================ /* * Copyright (C) 2025 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 * * https://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.squareup.moshi.kotlin.reflect import java.lang.reflect.Field import java.lang.reflect.Method import java.lang.reflect.Type import kotlin.metadata.KmClass import kotlin.metadata.KmClassifier import kotlin.metadata.KmClassifier.TypeAlias import kotlin.metadata.KmClassifier.TypeParameter import kotlin.metadata.KmProperty import kotlin.metadata.KmType import kotlin.metadata.KmValueParameter import kotlin.metadata.declaresDefaultValue import kotlin.metadata.isLocalClassName import kotlin.metadata.isNullable private fun defaultPrimitiveValue(type: Type): Any? = if (type is Class<*> && type.isPrimitive) { when (type) { Boolean::class.java -> false Char::class.java -> 0.toChar() Byte::class.java -> 0.toByte() Short::class.java -> 0.toShort() Int::class.java -> 0 Float::class.java -> 0f Long::class.java -> 0L Double::class.java -> 0.0 Void.TYPE -> throw IllegalStateException("Parameter with void type is illegal") else -> throw UnsupportedOperationException("Unknown primitive: $type") } } else { null } internal val KmType.canonicalName: String get() { return buildString { val classifierString = when (val cl = classifier) { is KmClassifier.Class -> createClassName(cl.name) is TypeAlias -> createClassName(cl.name) is TypeParameter -> arguments[cl.id].type?.canonicalName ?: "*" } append(classifierString) val args = arguments.joinToString(", ") { "${it.variance?.name.orEmpty()} ${it.type?.canonicalName ?: "*"}".trim() } if (args.isNotBlank()) { append('<') append(args) append('>') } } } /** * Creates a canonical class name as represented in Metadata's [kotlin.metadata.ClassName], where * package names in this name are separated by '/' and class names are separated by '.'. * * Example ClassName that we want to canonicalize: `"java/util/Map.Entry"`. * * Local classes are prefixed with ".", but for Moshi's use case we don't deal with those. */ private fun createClassName(kotlinMetadataName: String): String { require(!kotlinMetadataName.isLocalClassName()) { "Local/anonymous classes are not supported: $kotlinMetadataName" } return kotlinMetadataName.replace("/", ".") } internal data class KtParameter( val km: KmValueParameter, val index: Int, val rawType: Class<*>, val annotations: List, val valueClassBoxer: Method? = null, val valueClassUnboxer: Method? = null, ) { val name get() = km.name val declaresDefaultValue get() = km.declaresDefaultValue val isNullable get() = km.type.isNullable val isValueClass get() = valueClassBoxer != null } internal data class KtConstructor(val type: Class<*>, val kmExecutable: KmExecutable<*>) { val isDefault: Boolean get() = kmExecutable.isDefault val parameters: List get() = kmExecutable.parameters fun callBy(argumentsMap: IndexedParameterMap): R { val arguments = ArrayList(parameters.size) var mask = 0 val masks = ArrayList(1) var index = 0 for (parameter in parameters) { if (index != 0 && index % Integer.SIZE == 0) { masks += mask mask = 0 index = 0 } val possibleArg = argumentsMap[parameter] val usePossibleArg = possibleArg != null || parameter in argumentsMap when { usePossibleArg -> { // If this parameter is a value class, we need to unbox it val actualArg = if (parameter.isValueClass && possibleArg != null) { // The possibleArg is the boxed value class instance // Call unbox-impl on the instance to get the underlying primitive value parameter.valueClassUnboxer!!.invoke(possibleArg) } else { possibleArg } arguments += actualArg } parameter.declaresDefaultValue -> { arguments += defaultPrimitiveValue(parameter.rawType) mask = mask or (1 shl (index % Integer.SIZE)) } else -> { throw IllegalArgumentException( "No argument provided for a required parameter: $parameter" ) } } index++ } // Add the final mask if we have default parameters if (isDefault) { masks += mask } @Suppress("UNCHECKED_CAST") return kmExecutable.newInstance(arguments.toTypedArray(), masks) as R } companion object { fun primary(rawType: Class<*>, kmClass: KmClass): KtConstructor? { val kmExecutable = KmExecutable(rawType, kmClass) ?: return null return KtConstructor(rawType, kmExecutable) } } } internal data class KtProperty( val km: KmProperty, val jvmField: Field?, val jvmGetter: Method?, val jvmSetter: Method?, val jvmAnnotationsMethod: Method?, val parameter: KtParameter?, val valueClassBoxer: Method? = null, val valueClassUnboxer: Method? = null, ) { init { jvmField?.isAccessible = true jvmGetter?.isAccessible = true jvmSetter?.isAccessible = true } val name get() = km.name private val rawJavaType = jvmField?.genericType ?: jvmGetter?.genericReturnType ?: jvmSetter?.genericParameterTypes[0] ?: error( "No type information available for property '${km.name}' with type '${km.returnType.canonicalName}'." ) /** * The Java type for this property. For value classes, this returns the boxed value class type, * not the underlying primitive type. */ val javaType: Type get() = if (isValueClass) { // For value classes, return the value class type, not the primitive type val boxerClass = valueClassBoxer!!.declaringClass boxerClass } else { rawJavaType } val isValueClass get() = valueClassBoxer != null val annotations: Set by lazy { val set = LinkedHashSet() jvmField?.annotations?.let { set += it } jvmGetter?.annotations?.let { set += it } jvmSetter?.annotations?.let { set += it } jvmAnnotationsMethod?.annotations?.let { set += it } parameter?.annotations?.let { set += it } set } } ================================================ FILE: moshi-kotlin/src/main/resources/META-INF/com.android.tools/proguard/moshi-metadata-reflect.pro ================================================ # When editing this file, update the following files as well: # - META-INF/com.android.tools/r8-from-1.6.0/moshi-metadata-reflect.pro # - META-INF/com.android.tools/r8-upto-1.6.0/moshi-metadata-reflect.pro # - META-INF/proguard/moshi-metadata-reflect.pro # Keep Metadata annotations so they can be parsed at runtime. -keep class kotlin.Metadata { *; } # Keep default constructor marker name for lookup in signatures. -keepnames class kotlin.jvm.internal.DefaultConstructorMarker # Keep implementations of service loaded interfaces -keep interface kotlin.metadata.internal.extensions.MetadataExtensions -keep class * implements kotlin.metadata.internal.extensions.MetadataExtensions { public protected *; } # Keep generic signatures and annotations at runtime. -keepattributes Signature,RuntimeVisible*Annotations ================================================ FILE: moshi-kotlin/src/main/resources/META-INF/com.android.tools/r8-from-1.6.0/moshi-metadata-reflect.pro ================================================ # When editing this file, update the following files as well: # - META-INF/com.android.tools/proguard/moshi-kotlin.pro # - META-INF/com.android.tools/r8-upto-1.6.0/moshi-kotlin.pro # - META-INF/proguard/moshi-kotlin.pro # Keep Metadata annotations so they can be parsed at runtime. -keep class kotlin.Metadata { *; } # Keep default constructor marker name for lookup in signatures. -keepnames class kotlin.jvm.internal.DefaultConstructorMarker # Keep generic signatures and annotations at runtime. # R8 requires InnerClasses and EnclosingMethod if you keepattributes Signature. -keepattributes InnerClasses,Signature,RuntimeVisible*Annotations,EnclosingMethod ================================================ FILE: moshi-kotlin/src/main/resources/META-INF/com.android.tools/r8-upto-1.6.0/moshi-metadata-reflect.pro ================================================ # When editing this file, update the following files as well: # - META-INF/com.android.tools/proguard/moshi-kotlin.pro # - META-INF/com.android.tools/r8-from-1.6.0/moshi-kotlin.pro # - META-INF/proguard/moshi-kotlin.pro # Keep Metadata annotations so they can be parsed at runtime. -keep class kotlin.Metadata { *; } # Keep default constructor marker name for lookup in signatures. -keepnames class kotlin.jvm.internal.DefaultConstructorMarker # Keep implementations of service loaded interfaces # R8 will automatically handle these these in 1.6+ -keep interface kotlin.metadata.internal.extensions.MetadataExtensions -keep class * implements kotlin.metadata.internal.extensions.MetadataExtensions { public protected *; } # Keep generic signatures and annotations at runtime. # R8 requires InnerClasses and EnclosingMethod if you keepattributes Signature. -keepattributes InnerClasses,Signature,RuntimeVisible*Annotations,EnclosingMethod ================================================ FILE: moshi-kotlin/src/test/java/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi.kotlin.reflect import com.google.common.truth.Truth.assertThat import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import org.junit.Test class KotlinJsonAdapterTest { @JsonClass(generateAdapter = true) class Data @Test fun fallsBackToReflectiveAdapterWithoutCodegen() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val adapter = moshi.adapter(Data::class.java) assertThat(adapter.toString()) .isEqualTo( "KotlinJsonAdapter(com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest.Data).nullSafe()" ) } } ================================================ FILE: moshi-kotlin-codegen/build.gradle.kts ================================================ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") id("com.google.devtools.ksp") id("com.vanniktech.maven.publish") } tasks.withType().configureEach { compilerOptions { optIn.add("com.squareup.moshi.kotlin.codegen.api.InternalMoshiCodegenApi") } } tasks.compileTestKotlin { compilerOptions { optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") } } tasks.test { // KSP2 needs more memory to run until 1.0.21 minHeapSize = "2048m" maxHeapSize = "2048m" // Disable the annoying GradleWorkerMain apps that pop up while running jvmArgs("-Djava.awt.headless=true") } dependencies { implementation(project(":moshi")) api(libs.kotlinpoet) implementation(libs.kotlinpoet.ksp) implementation(libs.asm) implementation(libs.autoService) ksp(libs.autoService.ksp) // KSP deps compileOnly(libs.ksp) compileOnly(libs.ksp.api) compileOnly(libs.kotlin.compilerEmbeddable) // Always force the latest KSP version to match the one we're compiling against testImplementation(libs.ksp) testImplementation(libs.ksp.api) testImplementation(libs.kotlin.compilerEmbeddable) testImplementation(libs.kotlin.annotationProcessingEmbeddable) testImplementation(libs.kotlinCompileTesting.ksp) // Copy these again as they're not automatically included since they're shaded testImplementation(project(":moshi")) testImplementation(kotlin("reflect")) testImplementation(libs.kotlinpoet.ksp) testImplementation(libs.junit) testImplementation(libs.truth) testImplementation(libs.kotlinCompileTesting) } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/AdapterGenerator.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.ARRAY import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.FileSpec import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.INT import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.NameAllocator import com.squareup.kotlinpoet.ParameterSpec import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeSpec import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asTypeName import com.squareup.kotlinpoet.joinToCode import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.ParameterOnly import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.ParameterProperty import com.squareup.moshi.kotlin.codegen.api.FromJsonComponent.PropertyOnly import java.lang.reflect.Constructor import java.lang.reflect.Type import org.objectweb.asm.Type as AsmType private const val MOSHI_UTIL_PACKAGE = "com.squareup.moshi.internal" private const val TO_STRING_PREFIX = "GeneratedJsonAdapter(" private const val TO_STRING_SIZE_BASE = TO_STRING_PREFIX.length + 1 // 1 is the closing paren /** Generates a JSON adapter for a target type. */ @InternalMoshiCodegenApi public class AdapterGenerator( private val target: TargetType, private val propertyList: List, ) { private companion object { private val INT_TYPE_BLOCK = CodeBlock.of("%T::class.javaPrimitiveType!!", INT) private val DEFAULT_CONSTRUCTOR_MARKER_TYPE_BLOCK = CodeBlock.of("%M!!", MemberName(MOSHI_UTIL_PACKAGE, "DEFAULT_CONSTRUCTOR_MARKER")) private val CN_MOSHI = Moshi::class.asClassName() private val CN_TYPE = Type::class.asClassName() private val COMMON_SUPPRESS = arrayOf( // https://github.com/square/moshi/issues/1023 "DEPRECATION", // Because we look it up reflectively "unused", "UNUSED_PARAMETER", // Because we include underscores "ClassName", // Because we generate redundant `out` variance for some generics and there's no way // for us to know when it's redundant. "REDUNDANT_PROJECTION", // Because we may generate redundant explicit types for local vars with default values. // Example: 'var fooSet: Boolean = false' "RedundantExplicitType", // NameAllocator will just add underscores to differentiate names, which Kotlin doesn't // like for stylistic reasons. "LocalVariableName", // KotlinPoet always generates explicit public modifiers for public members. "RedundantVisibilityModifier", // For LambdaTypeNames we have to import kotlin.functions.* types "PLATFORM_CLASS_MAPPED_TO_KOTLIN", // Cover for calling fromJson() on a Nothing property type. Theoretically nonsensical but // we // support it "IMPLICIT_NOTHING_TYPE_ARGUMENT_IN_RETURN_POSITION", // Default values use bits to track assignment, and sometimes call toInt() on something // which // is already an int. "REDUNDANT_CALL_OF_CONVERSION_METHOD", ) .let { suppressions -> AnnotationSpec.builder(Suppress::class) .useSiteTarget(FILE) .addMember(suppressions.indices.joinToString { "%S" }, *suppressions) .build() } } private val nonTransientProperties = propertyList.filterNot { it.isIgnored } private val className = target.typeName.rawType() private val visibility = target.visibility private val typeVariables = target.typeVariables private val typeVariableResolver = typeVariables.toTypeVariableResolver() private val targetConstructorParams = target.constructor.parameters.mapKeys { (_, param) -> param.index } private val nameAllocator = NameAllocator() private val adapterName = "${className.simpleNames.joinToString(separator = "_")}JsonAdapter" private val originalTypeName = target.typeName.stripTypeVarVariance(typeVariableResolver) private val originalRawTypeName = originalTypeName.rawType() private val moshiParam = ParameterSpec.builder(nameAllocator.newName("moshi"), CN_MOSHI).build() private val typesParam = ParameterSpec.builder(nameAllocator.newName("types"), ARRAY.parameterizedBy(CN_TYPE)).build() private val readerParam = ParameterSpec.builder(nameAllocator.newName("reader"), JsonReader::class).build() private val writerParam = ParameterSpec.builder(nameAllocator.newName("writer"), JsonWriter::class).build() // Don't use NameAllocator here because it will add `_` to the name since it's a keyword, and that // results in it warning about not matching the overridden function's params. // https://github.com/square/moshi/issues/1502 private val valueParam = ParameterSpec.builder("value", originalTypeName.copy(nullable = true)).build() private val jsonAdapterTypeName = JsonAdapter::class.asClassName().parameterizedBy(originalTypeName.copy(nullable = true)) // selectName() API setup private val optionsProperty = PropertySpec.builder( nameAllocator.newName("options"), JsonReader.Options::class.asTypeName(), KModifier.PRIVATE, ) .initializer( "%T.of(%L)", JsonReader.Options::class.asTypeName(), nonTransientProperties.map { CodeBlock.of("%S", it.jsonName) }.joinToCode(", "), ) .build() private val constructorProperty = PropertySpec.builder( nameAllocator.newName("constructorRef"), Constructor::class.asClassName().parameterizedBy(originalTypeName).copy(nullable = true), KModifier.PRIVATE, ) .addAnnotation(Volatile::class) .mutable(true) .initializer("null") .build() public fun prepare( generateProguardRules: Boolean, typeHook: (TypeSpec) -> TypeSpec = { it }, ): PreparedAdapter { val reservedSimpleNames = mutableSetOf() for (property in nonTransientProperties) { // Allocate names for simple property types first to avoid collisions // See https://github.com/square/moshi/issues/1277 property.target.type.findRawType()?.simpleName?.let { simpleNameToReserve -> if (reservedSimpleNames.add(simpleNameToReserve)) { nameAllocator.newName(simpleNameToReserve) } } property.allocateNames(nameAllocator) } val generatedAdapter = generateType().let(typeHook) val result = FileSpec.builder(className.packageName, adapterName) result.addFileComment("Code generated by moshi-kotlin-codegen. Do not edit.") result.addAnnotation(COMMON_SUPPRESS) result.addType(generatedAdapter) val proguardConfig = if (generateProguardRules) { generatedAdapter.createProguardRule() } else { null } return PreparedAdapter(result.build(), proguardConfig) } private fun TypeSpec.createProguardRule(): ProguardConfig { val adapterConstructorParams = when (requireNotNull(primaryConstructor).parameters.size) { 1 -> listOf(CN_MOSHI.reflectionName()) 2 -> listOf(CN_MOSHI.reflectionName(), "${CN_TYPE.reflectionName()}[]") // Should never happen else -> error("Unexpected number of arguments on primary constructor: $primaryConstructor") } var hasDefaultProperties = false var parameterTypes = emptyList() target.constructor.signature?.let { constructorSignature -> if (constructorSignature.startsWith("constructor-impl")) { // Inline class, we don't support this yet. // This is a static method with signature like 'constructor-impl(I)I' return@let } hasDefaultProperties = propertyList.any { it.hasDefault } parameterTypes = AsmType.getArgumentTypes(constructorSignature.removePrefix("")).map { it.toReflectionString() } } return ProguardConfig( targetClass = className, adapterName = adapterName, adapterConstructorParams = adapterConstructorParams, targetConstructorHasDefaults = hasDefaultProperties, targetConstructorParams = parameterTypes, ) } private fun generateType(): TypeSpec { val result = TypeSpec.classBuilder(adapterName) result.superclass(jsonAdapterTypeName) if (typeVariables.isNotEmpty()) { result.addTypeVariables( typeVariables.map { it.stripTypeVarVariance(typeVariableResolver) as TypeVariableName } ) // require(types.size == 1) { // "TypeVariable mismatch: Expecting 1 type(s) for generic type variables [T], but received // ${types.size} with values $types" // } result.addInitializerBlock( CodeBlock.builder() .beginControlFlow("require(types.size == %L)", typeVariables.size) .addStatement( "buildString·{·append(%S).append(%L).append(%S).append(%S).append(%S).append(%L)·}", "TypeVariable mismatch: Expecting ", typeVariables.size, " ${if (typeVariables.size == 1) "type" else "types"} for generic type variables [", typeVariables.joinToString(", ") { it.name }, "], but received ", "${typesParam.name}.size", ) .endControlFlow() .build() ) } // TODO make this configurable. Right now it just matches the source model if (visibility == KModifier.INTERNAL) { result.addModifiers(KModifier.INTERNAL) } result.primaryConstructor(generateConstructor()) val typeRenderer: TypeRenderer = object : TypeRenderer() { override fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock { // Match only by name because equality checks for more things than just the name. For // example, a base class // may declare "T" but the subclass declares "T : Number", which is legal but will fail an // equals() test. val index = typeVariables.indexOfFirst { it.name == typeVariable.name } check(index != -1) { "Unexpected type variable $typeVariable" } return CodeBlock.of("%N[%L]", typesParam, index) } } // For inline types, we don't need the options property since we read the value directly if (!target.isInline) { result.addProperty(optionsProperty) } for (uniqueAdapter in nonTransientProperties.distinctBy { it.delegateKey }) { result.addProperty( uniqueAdapter.delegateKey.generateProperty( nameAllocator, typeRenderer, moshiParam, uniqueAdapter.name, ) ) } result.addFunction(generateToStringFun()) result.addFunction(generateFromJsonFun(target.isInline, result)) result.addFunction(generateToJson(target.isInline)) return result.build() } private fun generateConstructor(): FunSpec { val result = FunSpec.constructorBuilder() result.addParameter(moshiParam) if (typeVariables.isNotEmpty()) { result.addParameter(typesParam) } return result.build() } private fun generateToStringFun(): FunSpec { val name = originalRawTypeName.simpleNames.joinToString(".") val size = TO_STRING_SIZE_BASE + name.length return FunSpec.builder("toString") .addModifiers(KModifier.OVERRIDE) .returns(String::class) .addStatement( "return %M(%L)·{ append(%S).append(%S).append('%L') }", MemberName("kotlin.text", "buildString"), size, TO_STRING_PREFIX, name, ")", ) .build() } private fun generateFromJsonFun(isInline: Boolean, classBuilder: TypeSpec.Builder): FunSpec { val result = FunSpec.builder("fromJson") .addModifiers(KModifier.OVERRIDE) .addParameter(readerParam) .returns(originalTypeName) return if (isInline) { generateFromJsonInline(result) } else { generateFromJsonRegular(classBuilder, result) } } private fun generateFromJsonRegular( classBuilder: TypeSpec.Builder, result: FunSpec.Builder, ): FunSpec { for (property in nonTransientProperties) { result.addCode("%L", property.generateLocalProperty()) if (property.hasLocalIsPresentName) { result.addCode("%L", property.generateLocalIsPresentProperty()) } } val propertiesByIndex = propertyList .asSequence() .filter { it.hasConstructorParameter } .associateBy { it.target.parameterIndex } val components = mutableListOf() // Add parameters (± properties) first, their index matters for ((index, parameter) in targetConstructorParams) { val property = propertiesByIndex[index] if (property == null) { components += ParameterOnly(parameter) } else { components += ParameterProperty(parameter, property) } } // Now add the remaining properties that aren't parameters for (property in propertyList) { if (property.target.parameterIndex in targetConstructorParams) { continue // Already handled } if (property.isIgnored) { continue // We don't care about these outside of constructor parameters } components += PropertyOnly(property) } // Calculate how many masks we'll need. Round up if it's not evenly divisible by 32 val propertyCount = targetConstructorParams.size val maskCount = if (propertyCount == 0) { 0 } else { (propertyCount + 31) / 32 } // Allocate mask names val maskNames = Array(maskCount) { index -> nameAllocator.newName("mask$index") } val maskAllSetValues = Array(maskCount) { -1 } val useDefaultsConstructor = components.filterIsInstance().any { it.parameter.hasDefault } if (useDefaultsConstructor) { // Initialize all our masks, defaulting to fully unset (-1) for (maskName in maskNames) { result.addStatement("var %L = -1", maskName) } } result.addStatement("%N.beginObject()", readerParam) result.beginControlFlow("while (%N.hasNext())", readerParam) result.beginControlFlow("when (%N.selectName(%N))", readerParam, optionsProperty) // We track property index and mask index separately, because mask index is based on _all_ // constructor arguments, while property index is only based on the index passed into // JsonReader.Options. var propertyIndex = 0 val constructorPropertyTypes = mutableListOf() // // Track important indices for masks. Masks generally increment with each parameter (including // transient). // // Mask name index is an index into the maskNames array we initialized above. // // Once the maskIndex reaches 32, we've filled up that mask and have to move to the next mask // name. Reset the maskIndex relative to here and continue incrementing. // var maskIndex = 0 var maskNameIndex = 0 val updateMaskIndexes = { maskIndex++ if (maskIndex == 32) { // Move to the next mask maskIndex = 0 maskNameIndex++ } } for (input in components) { if (input is ParameterOnly || (input is ParameterProperty && input.property.isIgnored)) { updateMaskIndexes() constructorPropertyTypes += input.type.asTypeBlock() continue } else if (input is PropertyOnly && input.property.isIgnored) { continue } // We've removed all parameter-only types by this point val property = (input as PropertyComponent).property // Proceed as usual if (property.hasLocalIsPresentName || property.hasConstructorDefault) { result.beginControlFlow("%L ->", propertyIndex) if (property.delegateKey.nullable) { result.addStatement( "%N = %N.fromJson(%N)", property.localName, nameAllocator[property.delegateKey], readerParam, ) } else { val exception = unexpectedNull(property, readerParam) result.addStatement( "%N = %N.fromJson(%N) ?: throw·%L", property.localName, nameAllocator[property.delegateKey], readerParam, exception, ) } if (property.hasConstructorDefault) { val inverted = (1 shl maskIndex).inv() if (input is ParameterComponent && input.parameter.hasDefault) { maskAllSetValues[maskNameIndex] = maskAllSetValues[maskNameIndex] and inverted } result.addComment("\$mask = \$mask and (1 shl %L).inv()", maskIndex) result.addStatement( "%1L = %1L and 0x%2L.toInt()", maskNames[maskNameIndex], Integer.toHexString(inverted), ) } else { // Presence tracker for a mutable property result.addStatement("%N = true", property.localIsPresentName) } result.endControlFlow() } else { if (property.delegateKey.nullable) { result.addStatement( "%L -> %N = %N.fromJson(%N)", propertyIndex, property.localName, nameAllocator[property.delegateKey], readerParam, ) } else { val exception = unexpectedNull(property, readerParam) result.addStatement( "%L -> %N = %N.fromJson(%N) ?: throw·%L", propertyIndex, property.localName, nameAllocator[property.delegateKey], readerParam, exception, ) } } if (property.hasConstructorParameter) { constructorPropertyTypes += property.target.type.asTypeBlock() } propertyIndex++ updateMaskIndexes() } result.beginControlFlow("-1 ->") result.addComment("Unknown name, skip it.") result.addStatement("%N.skipName()", readerParam) result.addStatement("%N.skipValue()", readerParam) result.endControlFlow() result.endControlFlow() // when result.endControlFlow() // while result.addStatement("%N.endObject()", readerParam) var separator = "\n" val resultName = nameAllocator.newName("result") val hasNonConstructorProperties = nonTransientProperties.any { !it.hasConstructorParameter } val returnOrResultAssignment = if (hasNonConstructorProperties) { // Save the result var for reuse result.addStatement("val %N: %T", resultName, originalTypeName) CodeBlock.of("%N = ", resultName) } else { CodeBlock.of("return·") } // Used to indicate we're in an if-block that's assigning our result value and // needs to be closed with endControlFlow var closeNextControlFlowInAssignment = false if (useDefaultsConstructor) { if (target.isValueClass) { // Special case for value classes with defaults // For value classes, we want to call the constructor directly, omitting arguments when // they weren't present in the JSON (according to the mask) so defaults can be used. val paramProperty = components.filterIsInstance().single() val maskName = maskNames.single() // Value classes only have one parameter val maskSetValue = maskAllSetValues.single() // return if (mask == allSetValue) Constructor(value) else Constructor() result.addCode( "return·if·(%L·== 0x%L.toInt())·{\n", maskName, Integer.toHexString(maskSetValue), ) result.addCode("⇥") result.addComment("Property was present, invoke constructor with the value") result.addCode("%T(\n", originalTypeName) result.addCode("⇥%N = %N", paramProperty.property.name, paramProperty.property.localName) if (paramProperty.property.isRequired) { result.addMissingPropertyCheck(paramProperty.property, readerParam) } else if (!paramProperty.type.isNullable) { result.addCode("·as·%T", paramProperty.type) } result.addCode("\n⇤)\n") result.addCode("⇤}·else·{\n") result.addCode("⇥") result.addComment("Property was absent, invoke constructor without argument to use default") result.addCode("%T()\n", originalTypeName) result.addCode("⇤}\n") // Early return for value classes, skip the rest of the constructor logic return result.build() } else { // Happy path - all parameters with defaults are set val allMasksAreSetBlock = maskNames .withIndex() .map { (index, maskName) -> CodeBlock.of("$maskName·== 0x${Integer.toHexString(maskAllSetValues[index])}.toInt()") } .joinToCode("·&& ") result.beginControlFlow("if (%L)", allMasksAreSetBlock) result.addComment("All parameters with defaults are set, invoke the constructor directly") result.addCode("«%L·%T(", returnOrResultAssignment, originalTypeName) var localSeparator = "\n" val paramsToSet = components.filterIsInstance().filterNot { it.property.isIgnored } // Set all non-transient property parameters for (input in paramsToSet) { result.addCode(localSeparator) val property = input.property result.addCode("%N = %N", property.name, property.localName) if (property.isRequired) { result.addMissingPropertyCheck(property, readerParam) } else if (!input.type.isNullable) { // Unfortunately incurs an intrinsic null-check even though we know it's set, but // maybe in the future we can use contracts to omit them. result.addCode("·as·%T", input.type) } localSeparator = ",\n" } result.addCode("\n»)\n") result.nextControlFlow("else") closeNextControlFlowInAssignment = true classBuilder.addProperty(constructorProperty) result.addComment("Reflectively invoke the synthetic defaults constructor") // Dynamic default constructor call val nonNullConstructorType = constructorProperty.type.copy(nullable = false) val args = constructorPropertyTypes .plus(0.until(maskCount).map { INT_TYPE_BLOCK }) // Masks, one every 32 params .plus( DEFAULT_CONSTRUCTOR_MARKER_TYPE_BLOCK ) // Default constructor marker is always last .joinToCode(", ") val coreLookupBlock = CodeBlock.of("%T::class.java.getDeclaredConstructor(%L)", originalRawTypeName, args) val lookupBlock = if (originalTypeName is ParameterizedTypeName) { CodeBlock.of("(%L·as·%T)", coreLookupBlock, nonNullConstructorType) } else { coreLookupBlock } val initializerBlock = CodeBlock.of("this.%1N·?: %2L.also·{ this.%1N·= it }", constructorProperty, lookupBlock) val localConstructorProperty = PropertySpec.builder(nameAllocator.newName("localConstructor"), nonNullConstructorType) .addAnnotation( AnnotationSpec.builder(Suppress::class).addMember("%S", "UNCHECKED_CAST").build() ) .initializer(initializerBlock) .build() result.addCode("%L", localConstructorProperty) result.addCode("«%L%N.newInstance(", returnOrResultAssignment, localConstructorProperty) } } else { // Standard constructor call. Don't omit generics for parameterized types even if they can be // inferred, as calculating the right condition for inference exceeds the value gained from // being less pedantic. result.addCode("«%L%T(", returnOrResultAssignment, originalTypeName) } for (input in components.filterIsInstance()) { result.addCode(separator) if (useDefaultsConstructor) { if (input is ParameterOnly || (input is ParameterProperty && input.property.isIgnored)) { // We have to use the default primitive for the available type in order for // invokeDefaultConstructor to properly invoke it. Just using "null" isn't safe because // the transient type may be a primitive type. // Inline a little comment for readability indicating which parameter is it's referring to result.addCode( "/*·%L·*/·%L", input.parameter.name, input.type.rawType().defaultPrimitiveValue(), ) } else { result.addCode("%N", (input as ParameterProperty).property.localName) } } else if (input !is ParameterOnly) { val property = (input as ParameterProperty).property result.addCode("%N = %N", property.name, property.localName) } if (input is PropertyComponent) { val property = input.property if (!property.isIgnored && property.isRequired) { result.addMissingPropertyCheck(property, readerParam) } } separator = ",\n" } if (useDefaultsConstructor) { // Add the masks and a null instance for the trailing default marker instance result.addCode( ",\n%L,\n/*·DefaultConstructorMarker·*/·null", maskNames.map { CodeBlock.of("%L", it) }.joinToCode(", "), ) } result.addCode("\n»)\n") // Close the result assignment control flow, if any if (closeNextControlFlowInAssignment) { result.endControlFlow() } // Assign properties not present in the constructor. for (property in nonTransientProperties) { if (property.hasConstructorParameter) { continue // Property already handled. } if (property.hasLocalIsPresentName) { result.beginControlFlow("if (%N)", property.localIsPresentName) result.addStatement("%N.%N = %N", resultName, property.name, property.localName) result.endControlFlow() } else { result.addStatement( "%1N.%2N = %3N ?: %1N.%2N", resultName, property.name, property.localName, ) } } if (hasNonConstructorProperties) { result.addStatement("return·%1N", resultName) } return result.build() } private fun unexpectedNull(property: PropertyGenerator, reader: ParameterSpec): CodeBlock { return CodeBlock.of( "%M(%S, %S, %N)", MemberName(MOSHI_UTIL_PACKAGE, "unexpectedNull"), property.localName, property.jsonName, reader, ) } private fun generateToJson(isInline: Boolean): FunSpec { val builder = FunSpec.builder("toJson") .addModifiers(KModifier.OVERRIDE) .addParameter(writerParam) .addParameter(valueParam) return if (isInline) { generateToJsonInline(builder) } else { generateToJsonRegular(builder) } } private fun generateToJsonRegular(builder: FunSpec.Builder): FunSpec { builder.beginControlFlow("if (%N == null)", valueParam) builder.addStatement( "throw·%T(%S)", NullPointerException::class, "${valueParam.name} was null! Wrap in .nullSafe() to write nullable values.", ) builder.endControlFlow() builder.addStatement("%N.beginObject()", writerParam) nonTransientProperties.forEach { property -> // We manually put in quotes because we know the jsonName is already escaped builder.addStatement("%N.name(%S)", writerParam, property.jsonName) builder.addStatement( "%N.toJson(%N, %N.%N)", nameAllocator[property.delegateKey], writerParam, valueParam, property.name, ) } builder.addStatement("%N.endObject()", writerParam) return builder.build() } /** Generates a fromJson function for inline types that reads the value directly. */ private fun generateFromJsonInline(builder: FunSpec.Builder): FunSpec { val property = nonTransientProperties.single() // Read the value directly if (property.delegateKey.nullable) { builder.addStatement( "val %N = %N.fromJson(%N)", property.localName, nameAllocator[property.delegateKey], readerParam, ) } else { val exception = unexpectedNull(property, readerParam) builder.addStatement( "val %N = %N.fromJson(%N) ?: throw·%L", property.localName, nameAllocator[property.delegateKey], readerParam, exception, ) } builder.addStatement("return %T(%N = %N)", originalTypeName, property.name, property.localName) return builder.build() } /** Generates a toJson function for inline types that writes the value directly. */ private fun generateToJsonInline(builder: FunSpec.Builder): FunSpec { builder.beginControlFlow("if (%N == null)", valueParam) builder.addStatement( "throw·%T(%S)", NullPointerException::class, "${valueParam.name} was null! Wrap in .nullSafe() to write nullable values.", ) builder.endControlFlow() val property = nonTransientProperties.single() builder.addStatement( "%N.toJson(%N, %N.%N)", nameAllocator[property.delegateKey], writerParam, valueParam, property.name, ) return builder.build() } } private fun FunSpec.Builder.addMissingPropertyCheck( property: PropertyGenerator, readerParam: ParameterSpec, ) { val missingPropertyBlock = CodeBlock.of( "%M(%S, %S, %N)", MemberName(MOSHI_UTIL_PACKAGE, "missingProperty"), property.localName, property.jsonName, readerParam, ) addCode(" ?: throw·%L", missingPropertyBlock) } /** Represents a prepared adapter with its [spec] and optional associated [proguardConfig]. */ @InternalMoshiCodegenApi public data class PreparedAdapter(val spec: FileSpec, val proguardConfig: ProguardConfig?) private fun AsmType.toReflectionString(): String { return when (this) { AsmType.VOID_TYPE -> "void" AsmType.BOOLEAN_TYPE -> "boolean" AsmType.CHAR_TYPE -> "char" AsmType.BYTE_TYPE -> "byte" AsmType.SHORT_TYPE -> "short" AsmType.INT_TYPE -> "int" AsmType.FLOAT_TYPE -> "float" AsmType.LONG_TYPE -> "long" AsmType.DOUBLE_TYPE -> "double" else -> when (sort) { AsmType.ARRAY -> "${elementType.toReflectionString()}[]" // Object type else -> className } } } private interface PropertyComponent { val property: PropertyGenerator val type: TypeName } private interface ParameterComponent { val parameter: TargetParameter val type: TypeName } /** * Type hierarchy for describing fromJson() components. Specifically - parameters, properties, and * parameter properties. All three of these scenarios participate in fromJson() parsing. */ private sealed class FromJsonComponent { abstract val type: TypeName data class ParameterOnly(override val parameter: TargetParameter) : FromJsonComponent(), ParameterComponent { override val type: TypeName = parameter.type } data class PropertyOnly(override val property: PropertyGenerator) : FromJsonComponent(), PropertyComponent { override val type: TypeName = property.target.type } data class ParameterProperty( override val parameter: TargetParameter, override val property: PropertyGenerator, ) : FromJsonComponent(), ParameterComponent, PropertyComponent { override val type: TypeName = parameter.type } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/DelegateKey.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.MemberName import com.squareup.kotlinpoet.NameAllocator import com.squareup.kotlinpoet.ParameterSpec import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.WildcardTypeName import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.joinToCode import com.squareup.moshi.JsonAdapter import java.util.Locale /** A JsonAdapter that can be used to encode and decode a particular field. */ @InternalMoshiCodegenApi public data class DelegateKey( private val type: TypeName, private val jsonQualifiers: List, ) { public val nullable: Boolean get() = type.isNullable /** Returns an adapter to use when encoding and decoding this property. */ internal fun generateProperty( nameAllocator: NameAllocator, typeRenderer: TypeRenderer, moshiParameter: ParameterSpec, propertyName: String, ): PropertySpec { val qualifierNames = jsonQualifiers.joinToString("") { "At${it.typeName.rawType().simpleName}" } val adapterName = nameAllocator.newName( "${type.toVariableName().replaceFirstChar { it.lowercase(Locale.US) }}${qualifierNames}Adapter", this, ) val nullableType = type.copy(nullable = true) val adapterTypeName = JsonAdapter::class.asClassName().parameterizedBy(nullableType) val standardArgs = arrayOf(moshiParameter, typeRenderer.render(type)) val (initializerString, args) = when { jsonQualifiers.isEmpty() -> ", %M()" to arrayOf(MemberName("kotlin.collections", "emptySet")) else -> { ", setOf(%L)" to arrayOf(jsonQualifiers.map { it.asInstantiationExpression() }.joinToCode()) } } val finalArgs = arrayOf(*standardArgs, *args, propertyName) return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE) .initializer("%N.adapter(%L$initializerString, %S)", *finalArgs) .build() } } private fun AnnotationSpec.asInstantiationExpression(): CodeBlock { // (args) return CodeBlock.of("%T(%L)", typeName, members.joinToCode()) } /** * Returns a suggested variable name derived from a list of type names. This just concatenates, * yielding types like MapOfStringLong. */ private fun List.toVariableNames() = joinToString("") { it.toVariableName() } /** Returns a suggested variable name derived from a type name, like nullableListOfString. */ private fun TypeName.toVariableName(): String { val base = when (this) { is ClassName -> simpleName is ParameterizedTypeName -> rawType.simpleName + "Of" + typeArguments.toVariableNames() is WildcardTypeName -> (inTypes + outTypes).toVariableNames() is TypeVariableName -> name + bounds.toVariableNames() else -> throw IllegalArgumentException("Unrecognized type! $this") } return if (isNullable) { "Nullable$base" } else { base } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/InternalMoshiCodegenApi.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.api /** Internal Moshi code gen APIs. */ @MustBeDocumented @Retention(value = AnnotationRetention.BINARY) @RequiresOptIn( level = RequiresOptIn.Level.WARNING, message = "This is an internal API and may change at any time.", ) public annotation class InternalMoshiCodegenApi ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/Options.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.ClassName @InternalMoshiCodegenApi public object Options { /** * This processing option can be specified to have a `@Generated` annotation included in the * generated code. It is not encouraged unless you need it for static analysis reasons and not * enabled by default. * * Note that this can only be one of the following values: * * `"javax.annotation.processing.Generated"` (JRE 9+) * * `"javax.annotation.Generated"` (JRE <9) */ public const val OPTION_GENERATED: String = "moshi.generated" /** * This boolean processing option can disable proguard rule generation. Normally, this is not * recommended unless end-users build their own JsonAdapter look-up tool. This is enabled by * default. */ public const val OPTION_GENERATE_PROGUARD_RULES: String = "moshi.generateProguardRules" /** * This boolean processing option controls whether or not Moshi will directly instantiate * JsonQualifier annotations in Kotlin 1.6+. Note that this is enabled by default in Kotlin 1.6 * but can be disabled to restore the legacy behavior of storing annotations on generated adapter * fields and looking them up reflectively. */ public const val OPTION_INSTANTIATE_ANNOTATIONS: String = "moshi.instantiateAnnotations" public val POSSIBLE_GENERATED_NAMES: Map = arrayOf( ClassName("javax.annotation.processing", "Generated"), ClassName("javax.annotation", "Generated"), ) .associateBy { it.canonicalName } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/ProguardRules.kt ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.ClassName /** * Represents a proguard configuration for a given spec. This covers three main areas: * - Keeping the target class name to Moshi's reflective lookup of the adapter. * - Keeping the generated adapter class name + public constructor for reflective lookup. * - Keeping any used JsonQualifier annotations and the properties they are attached to. * - If the target class has default parameter values, also keeping the associated synthetic * constructor as well as the DefaultConstructorMarker type Kotlin adds to it. * * Each rule is intended to be as specific and targeted as possible to reduce footprint, and each is * conditioned on usage of the original target type (implicitly for keepnames and keepclassmembers, * which have no effect if the target type was removed in the shrinking phase). * * To keep this processor as an ISOLATING incremental processor, we generate one file per target * class with a deterministic name (see [outputFilePathWithoutExtension]) with an appropriate * originating element. */ @InternalMoshiCodegenApi public data class ProguardConfig( val targetClass: ClassName, val adapterName: String, val adapterConstructorParams: List, val targetConstructorHasDefaults: Boolean, val targetConstructorParams: List, ) { public fun outputFilePathWithoutExtension(canonicalName: String): String { return "META-INF/proguard/moshi-$canonicalName" } public fun writeTo(out: Appendable): Unit = out.run { // // -keepnames class {the target class} // -if class {the target class} // -keep class {the generated adapter} { // (...); // private final {adapter fields} // } // val targetName = targetClass.reflectionName() val adapterCanonicalName = ClassName(targetClass.packageName, adapterName).canonicalName // Keep the class name for Moshi's reflective lookup based on it appendLine("-keepnames class $targetName") appendLine("-if class $targetName") appendLine("-keep class $adapterCanonicalName {") // Keep the constructor for Moshi's reflective lookup val constructorArgs = adapterConstructorParams.joinToString(",") appendLine(" public ($constructorArgs);") appendLine("}") if (targetConstructorHasDefaults) { // If the target class has default parameter values, keep its synthetic constructor // // -if class {the target class} // -keepnames class kotlin.jvm.internal.DefaultConstructorMarker // -keepclassmembers @com.squareup.moshi.JsonClass @kotlin.Metadata class * { // synthetic (...); // } // appendLine("-if class $targetName") appendLine("-keepnames class kotlin.jvm.internal.DefaultConstructorMarker") appendLine("-keepclassmembers class $targetName {") val allParams = targetConstructorParams.toMutableList() val maskCount = if (targetConstructorParams.isEmpty()) { 0 } else { (targetConstructorParams.size + 31) / 32 } repeat(maskCount) { allParams += "int" } allParams += "kotlin.jvm.internal.DefaultConstructorMarker" val params = allParams.joinToString(",") appendLine(" public synthetic ($params);") appendLine("}") } } } /** * Represents a qualified property with its [name] in the adapter fields and list of [qualifiers] * associated with it. */ @InternalMoshiCodegenApi public data class QualifierAdapterProperty(val name: String, val qualifiers: Set) ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/PropertyGenerator.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.NameAllocator import com.squareup.kotlinpoet.PropertySpec /** Generates functions to encode and decode a property as JSON. */ @InternalMoshiCodegenApi public class PropertyGenerator( public val target: TargetProperty, public val delegateKey: DelegateKey, public val isIgnored: Boolean = false, ) { public val name: String = target.name public val jsonName: String = target.jsonName ?: target.name public val hasDefault: Boolean = target.hasDefault public lateinit var localName: String public lateinit var localIsPresentName: String public val isRequired: Boolean get() = !delegateKey.nullable && !hasDefault public val hasConstructorParameter: Boolean get() = target.parameterIndex != -1 /** * IsPresent is required if the following conditions are met: * - Is not transient * - Has a default * - Is not a constructor parameter (for constructors we use a defaults mask) * - Is nullable (because we differentiate absent from null) * * This is used to indicate that presence should be checked first before possible assigning null * to an absent value */ public val hasLocalIsPresentName: Boolean = !isIgnored && hasDefault && !hasConstructorParameter && delegateKey.nullable public val hasConstructorDefault: Boolean = hasDefault && hasConstructorParameter internal fun allocateNames(nameAllocator: NameAllocator) { localName = nameAllocator.newName(name) localIsPresentName = nameAllocator.newName("${name}Set") } internal fun generateLocalProperty(): PropertySpec { return PropertySpec.builder(localName, target.type.copy(nullable = true)) .mutable(true) .apply { if (hasConstructorDefault) { // We default to the primitive default type, as reflectively invoking the constructor // without this (even though it's a throwaway) will fail argument type resolution in // the reflective invocation. initializer(target.type.defaultPrimitiveValue()) } else { initializer("null") } } .build() } internal fun generateLocalIsPresentProperty(): PropertySpec { return PropertySpec.builder(localIsPresentName, BOOLEAN) .mutable(true) .initializer("false") .build() } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetConstructor.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.KModifier /** A constructor in user code that should be called by generated code. */ @InternalMoshiCodegenApi public data class TargetConstructor( val parameters: LinkedHashMap, val visibility: KModifier, val signature: String?, ) { init { visibility.checkIsVisibility() } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetParameter.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.TypeName /** A parameter in user code that should be populated by generated code. */ @InternalMoshiCodegenApi public data class TargetParameter( val name: String, val index: Int, val type: TypeName, val hasDefault: Boolean, val jsonName: String? = null, val jsonIgnore: Boolean = false, val qualifiers: Set? = null, ) ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetProperty.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName /** A property in user code that maps to JSON. */ @InternalMoshiCodegenApi public data class TargetProperty( val propertySpec: PropertySpec, val parameter: TargetParameter?, val visibility: KModifier, val jsonName: String?, val jsonIgnore: Boolean, ) { val name: String get() = propertySpec.name val type: TypeName get() = propertySpec.type val parameterIndex: Int get() = parameter?.index ?: -1 val hasDefault: Boolean get() = parameter?.hasDefault ?: true override fun toString(): String = name } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TargetType.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeVariableName /** A user type that should be decoded and encoded by generated code. */ @InternalMoshiCodegenApi public data class TargetType( val typeName: TypeName, val constructor: TargetConstructor, val properties: Map, val typeVariables: List, val isDataClass: Boolean, val visibility: KModifier, val isValueClass: Boolean, val isInline: Boolean = false, ) { init { visibility.checkIsVisibility() } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/TypeRenderer.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.ARRAY import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.BYTE import com.squareup.kotlinpoet.CHAR import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.DOUBLE import com.squareup.kotlinpoet.FLOAT import com.squareup.kotlinpoet.INT import com.squareup.kotlinpoet.LONG import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.SHORT import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.WildcardTypeName import com.squareup.moshi.Types /** * Renders literals like `Types.newParameterizedType(List::class.java, String::class.java)`. * Rendering is pluggable so that type variables can either be resolved or emitted as other code * blocks. */ internal abstract class TypeRenderer { abstract fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock fun render(typeName: TypeName, forceBox: Boolean = false): CodeBlock { if (typeName.annotations.isNotEmpty()) { return render(typeName.copy(annotations = emptyList()), forceBox) } if (typeName.isNullable) { return renderObjectType(typeName.copy(nullable = false)) } return when (typeName) { is ClassName -> { if (forceBox) { renderObjectType(typeName) } else { CodeBlock.of("%T::class.java", typeName) } } is ParameterizedTypeName -> { // If it's an Array type, we shortcut this to return Types.arrayOf() if (typeName.rawType == ARRAY) { CodeBlock.of("%T.arrayOf(%L)", Types::class, renderObjectType(typeName.typeArguments[0])) } else { val builder = CodeBlock.builder().apply { add("%T.", Types::class) val enclosingClassName = typeName.rawType.enclosingClassName() if (enclosingClassName != null) { add("newParameterizedTypeWithOwner(%L, ", render(enclosingClassName)) } else { add("newParameterizedType(") } add("%T::class.java", typeName.rawType) for (typeArgument in typeName.typeArguments) { add(", %L", renderObjectType(typeArgument)) } add(")") } builder.build() } } is WildcardTypeName -> { val target: TypeName val method: String when { typeName.inTypes.size == 1 -> { target = typeName.inTypes[0] method = "supertypeOf" } typeName.outTypes.size == 1 -> { target = typeName.outTypes[0] method = "subtypeOf" } else -> throw IllegalArgumentException( "Unrepresentable wildcard type. Cannot have more than one bound: $typeName" ) } CodeBlock.of("%T.%L(%L)", Types::class, method, render(target, forceBox = true)) } is TypeVariableName -> renderTypeVariable(typeName) else -> throw IllegalArgumentException("Unrepresentable type: $typeName") } } private fun renderObjectType(typeName: TypeName): CodeBlock { return if (typeName.isPrimitive()) { CodeBlock.of("%T::class.javaObjectType", typeName) } else { render(typeName) } } private fun TypeName.isPrimitive(): Boolean { return when (this) { BOOLEAN, BYTE, SHORT, INT, LONG, CHAR, FLOAT, DOUBLE -> true else -> false } } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/kotlintypes.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.ANY import com.squareup.kotlinpoet.ARRAY import com.squareup.kotlinpoet.BOOLEAN import com.squareup.kotlinpoet.BYTE import com.squareup.kotlinpoet.CHAR import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.DOUBLE import com.squareup.kotlinpoet.DelicateKotlinPoetApi import com.squareup.kotlinpoet.FLOAT import com.squareup.kotlinpoet.INT import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.LONG import com.squareup.kotlinpoet.LambdaTypeName import com.squareup.kotlinpoet.NOTHING import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.SHORT import com.squareup.kotlinpoet.STAR import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.UNIT import com.squareup.kotlinpoet.WildcardTypeName import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.asTypeName import java.lang.reflect.Array internal fun TypeName.rawType(): ClassName { return findRawType() ?: throw IllegalArgumentException("Cannot get raw type from $this") } internal fun TypeName.findRawType(): ClassName? { return when (this) { is ClassName -> this is ParameterizedTypeName -> rawType is LambdaTypeName -> { var count = parameters.size if (receiver != null) { count++ } val functionSimpleName = if (count >= 23) { "FunctionN" } else { "Function$count" } ClassName("kotlin.jvm.functions", functionSimpleName) } else -> null } } internal fun TypeName.defaultPrimitiveValue(): CodeBlock = when (this) { BOOLEAN -> CodeBlock.of("false") CHAR -> CodeBlock.of("0.toChar()") BYTE -> CodeBlock.of("0.toByte()") SHORT -> CodeBlock.of("0.toShort()") INT -> CodeBlock.of("0") FLOAT -> CodeBlock.of("0f") LONG -> CodeBlock.of("0L") DOUBLE -> CodeBlock.of("0.0") UNIT, Void::class.asTypeName(), NOTHING -> throw IllegalStateException("Parameter with void, Unit, or Nothing type is illegal") else -> CodeBlock.of("null") } @OptIn(DelicateKotlinPoetApi::class) internal fun TypeName.asTypeBlock(): CodeBlock { if (annotations.isNotEmpty()) { return copy(annotations = emptyList()).asTypeBlock() } when (this) { is ParameterizedTypeName -> { return if (rawType == ARRAY) { val componentType = typeArguments[0] if (componentType is ParameterizedTypeName) { // "generic" array just uses the component's raw type // java.lang.reflect.Array.newInstance(, 0).javaClass CodeBlock.of( "%T.newInstance(%L, 0).javaClass", Array::class.java.asClassName(), componentType.rawType.asTypeBlock(), ) } else { CodeBlock.of("%T::class.java", copy(nullable = false)) } } else { rawType.asTypeBlock() } } is TypeVariableName -> { val bound = bounds.firstOrNull() ?: ANY return bound.asTypeBlock() } is LambdaTypeName -> return rawType().asTypeBlock() is ClassName -> { // Check against the non-nullable version for equality, but we'll keep the nullability in // consideration when creating the CodeBlock if needed. return when (copy(nullable = false)) { BOOLEAN, CHAR, BYTE, SHORT, INT, FLOAT, LONG, DOUBLE -> { if (isNullable) { // Remove nullable but keep the java object type CodeBlock.of("%T::class.javaObjectType", copy(nullable = false)) } else { CodeBlock.of("%T::class.javaPrimitiveType!!", this) } } UNIT, Void::class.asTypeName(), NOTHING -> throw IllegalStateException("Parameter with void, Unit, or Nothing type is illegal") else -> CodeBlock.of("%T::class.java", copy(nullable = false)) } } else -> throw UnsupportedOperationException( "Parameter with type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, or type variables are allowed." ) } } internal fun KModifier.checkIsVisibility() { require(ordinal <= ordinal) { "Visibility must be one of ${(0..ordinal).joinToString { KModifier.values()[it].name }}. Is $name" } } internal fun TypeName.stripTypeVarVariance(resolver: TypeVariableResolver): TypeName { return when (this) { is ClassName -> this is ParameterizedTypeName -> { deepCopy { it.stripTypeVarVariance(resolver) } } is TypeVariableName -> resolver[name] is WildcardTypeName -> deepCopy { it.stripTypeVarVariance(resolver) } else -> throw UnsupportedOperationException( "Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed." ) } } internal fun ParameterizedTypeName.deepCopy( transform: (TypeName) -> TypeName ): ParameterizedTypeName { return rawType .parameterizedBy(typeArguments.map { transform(it) }) .copy(nullable = isNullable, annotations = annotations, tags = tags) } internal fun TypeVariableName.deepCopy( variance: KModifier? = this.variance, transform: (TypeName) -> TypeName, ): TypeVariableName { return TypeVariableName(name = name, bounds = bounds.map { transform(it) }, variance = variance) .copy(nullable = isNullable, annotations = annotations, tags = tags) } internal fun WildcardTypeName.deepCopy(transform: (TypeName) -> TypeName): TypeName { // TODO Would be nice if KotlinPoet modeled these easier. // Producer type - empty inTypes, single element outTypes // Consumer type - single element inTypes, single ANY element outType. return when { this == STAR -> this outTypes.isNotEmpty() && inTypes.isEmpty() -> { WildcardTypeName.producerOf(transform(outTypes[0])) .copy(nullable = isNullable, annotations = annotations) } inTypes.isNotEmpty() -> { WildcardTypeName.consumerOf(transform(inTypes[0])) .copy(nullable = isNullable, annotations = annotations) } else -> throw UnsupportedOperationException("Not possible.") } } internal fun LambdaTypeName.deepCopy(transform: (TypeName) -> TypeName): TypeName { return LambdaTypeName.get( receiver?.let(transform), parameters.map { it.toBuilder(type = transform(it.type)).build() }, transform(returnType), ) .copy(nullable = isNullable, annotations = annotations, suspending = isSuspending) } internal interface TypeVariableResolver { val parametersMap: Map operator fun get(index: String): TypeVariableName } internal fun List.toTypeVariableResolver( fallback: TypeVariableResolver? = null, sourceType: String? = null, ): TypeVariableResolver { val parametersMap = LinkedHashMap() val typeParamResolver = { id: String -> parametersMap[id] ?: fallback?.get(id) ?: throw IllegalStateException("No type argument found for $id! Anaylzing $sourceType") } val resolver = object : TypeVariableResolver { override val parametersMap: Map = parametersMap override operator fun get(index: String): TypeVariableName = typeParamResolver(index) } // Fill the parametersMap. Need to do sequentially and allow for referencing previously defined // params for (typeVar in this) { check(typeVar is TypeVariableName) // Put the simple typevar in first, then it can be referenced in the full toTypeVariable() // replacement later that may add bounds referencing this. val id = typeVar.name parametersMap[id] = TypeVariableName(id) } for (typeVar in this) { check(typeVar is TypeVariableName) // Now replace it with the full version. parametersMap[typeVar.name] = typeVar.deepCopy(null) { it.stripTypeVarVariance(resolver) } } return resolver } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/api/typeAliasUnwrapping.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.api import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.Dynamic import com.squareup.kotlinpoet.LambdaTypeName import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.TypeVariableName import com.squareup.kotlinpoet.WildcardTypeName import com.squareup.kotlinpoet.tag import com.squareup.kotlinpoet.tags.TypeAliasTag import java.util.TreeSet private fun TypeName.unwrapTypeAliasInternal(): TypeName? { return tag()?.abbreviatedType?.let { unwrappedType -> // Keep track of all annotations across type levels. Sort them too for consistency. val runningAnnotations = TreeSet(compareBy { it.toString() }).apply { addAll(annotations) } val nestedUnwrappedType = unwrappedType.unwrapTypeAlias() runningAnnotations.addAll(nestedUnwrappedType.annotations) // If any type is nullable, then the whole thing is nullable val isAnyNullable = isNullable || nestedUnwrappedType.isNullable nestedUnwrappedType.copy(nullable = isAnyNullable, annotations = runningAnnotations.toList()) } } internal fun TypeName.unwrapTypeAlias(): TypeName { return when (this) { is ClassName -> unwrapTypeAliasInternal() ?: this is ParameterizedTypeName -> { unwrapTypeAliasInternal() ?: deepCopy(TypeName::unwrapTypeAlias) } is TypeVariableName -> { unwrapTypeAliasInternal() ?: deepCopy(transform = TypeName::unwrapTypeAlias) } is WildcardTypeName -> { unwrapTypeAliasInternal() ?: deepCopy(TypeName::unwrapTypeAlias) } is LambdaTypeName -> { unwrapTypeAliasInternal() ?: deepCopy(TypeName::unwrapTypeAlias) } Dynamic -> throw UnsupportedOperationException( "Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed." ) } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/AppliedType.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.ksp import com.google.devtools.ksp.getAllSuperTypes import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.ClassKind.CLASS import com.google.devtools.ksp.symbol.KSClassDeclaration import com.squareup.kotlinpoet.ANY import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.asClassName import com.squareup.kotlinpoet.ksp.toClassName private val OBJECT_CLASS = java.lang.Object::class.asClassName() /** * A concrete type like `List` with enough information to know how to resolve its type * variables. */ internal class AppliedType private constructor(val type: KSClassDeclaration, val typeName: TypeName = type.toClassName()) { /** * Returns all super classes of this, recursively. Only [CLASS] is used as we can't really use * other types. */ fun superclasses(resolver: Resolver): LinkedHashSet { val result: LinkedHashSet = LinkedHashSet() result.add(this) for (supertype in type.getAllSuperTypes()) { val decl = supertype.declaration check(decl is KSClassDeclaration) if (decl.classKind != CLASS) { // Don't load properties for interface types. continue } val qualifiedName = decl.qualifiedName val superTypeKsClass = resolver.getClassDeclarationByName(qualifiedName!!)!! val typeName = decl.toClassName() if (typeName == ANY || typeName == OBJECT_CLASS) { // Don't load properties for kotlin.Any/java.lang.Object. continue } result.add(AppliedType(superTypeKsClass, typeName)) } return result } override fun toString() = type.qualifiedName!!.asString() companion object { operator fun invoke(type: KSClassDeclaration): AppliedType { return AppliedType(type) } } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorProvider.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.ksp import com.google.auto.service.AutoService import com.google.devtools.ksp.processing.CodeGenerator import com.google.devtools.ksp.processing.Dependencies import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.processing.SymbolProcessor import com.google.devtools.ksp.processing.SymbolProcessorEnvironment import com.google.devtools.ksp.processing.SymbolProcessorProvider import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSDeclaration import com.google.devtools.ksp.symbol.KSFile import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.ksp.addOriginatingKSFile import com.squareup.kotlinpoet.ksp.writeTo import com.squareup.moshi.JsonClass import com.squareup.moshi.kotlin.codegen.api.AdapterGenerator import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATED import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_PROGUARD_RULES import com.squareup.moshi.kotlin.codegen.api.Options.POSSIBLE_GENERATED_NAMES import com.squareup.moshi.kotlin.codegen.api.ProguardConfig import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator import java.io.OutputStreamWriter import java.nio.charset.StandardCharsets @AutoService(SymbolProcessorProvider::class) public class JsonClassSymbolProcessorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { return JsonClassSymbolProcessor(environment) } } private class JsonClassSymbolProcessor(environment: SymbolProcessorEnvironment) : SymbolProcessor { private companion object { val JSON_CLASS_NAME = JsonClass::class.qualifiedName!! } private val codeGenerator = environment.codeGenerator private val logger = environment.logger private val generatedOption = environment.options[OPTION_GENERATED]?.also { logger.check(it in POSSIBLE_GENERATED_NAMES) { "Invalid option value for $OPTION_GENERATED. Found $it, allowable values are ${POSSIBLE_GENERATED_NAMES.keys}." } } private val generateProguardRules = environment.options[OPTION_GENERATE_PROGUARD_RULES]?.toBooleanStrictOrNull() ?: true override fun process(resolver: Resolver): List { val generatedAnnotation = generatedOption?.let { AnnotationSpec.builder(ClassName.bestGuess(it)) .addMember("value = [%S]", JsonClassSymbolProcessor::class.java.canonicalName) .addMember("comments = %S", "https://github.com/square/moshi") .build() } for (type in resolver.getSymbolsWithAnnotation(JSON_CLASS_NAME)) { // For the smart cast if (type !is KSDeclaration) { logger.error("@JsonClass can't be applied to $type: must be a Kotlin class", type) continue } val jsonClassAnnotation = type.findAnnotationWithType() ?: continue val generator = jsonClassAnnotation.generator if (generator.isNotEmpty()) continue if (!jsonClassAnnotation.generateAdapter) continue val isInline = jsonClassAnnotation.inline try { val originatingFile = type.containingFile!! val adapterGenerator = adapterGenerator(logger, resolver, type, isInline) ?: return emptyList() val preparedAdapter = adapterGenerator.prepare(generateProguardRules) { spec -> spec .toBuilder() .apply { generatedAnnotation?.let(::addAnnotation) } .addOriginatingKSFile(originatingFile) .build() } preparedAdapter.spec.writeTo(codeGenerator, aggregating = false) preparedAdapter.proguardConfig?.writeTo(codeGenerator, originatingFile) } catch (e: Exception) { logger.error( "Error preparing ${type.simpleName.asString()}: ${e.stackTrace.joinToString("\n")}" ) } } return emptyList() } private fun adapterGenerator( logger: KSPLogger, resolver: Resolver, originalType: KSDeclaration, isInline: Boolean, ): AdapterGenerator? { val type = targetType(originalType, resolver, logger, isInline) ?: return null val properties = mutableMapOf() for (property in type.properties.values) { val generator = property.generator(logger, resolver, originalType) if (generator != null) { properties[property.name] = generator } } // Validate inline types have exactly one non-transient property that is not nullable if (isInline) { val nonIgnoredBindings = properties.values.filterNot { it.isIgnored } if (nonIgnoredBindings.size != 1) { logger.error( "@JsonClass with inline = true requires exactly one non-transient property, " + "but ${originalType.simpleName.asString()} has ${nonIgnoredBindings.size}: " + "${nonIgnoredBindings.joinToString { it.name }}.", originalType, ) return null } val inlineProperty = nonIgnoredBindings[0] if (inlineProperty.delegateKey.nullable) { logger.error( "@JsonClass with inline = true requires a non-nullable property, " + "but ${originalType.simpleName.asString()}.${inlineProperty.name} is nullable.", originalType, ) return null } } for ((name, parameter) in type.constructor.parameters) { if (type.properties[parameter.name] == null && !parameter.hasDefault) { // TODO would be nice if we could pass the parameter node directly? logger.error("No property for required constructor parameter $name", originalType) return null } } // Sort properties so that those with constructor parameters come first. val sortedProperties = properties.values.sortedBy { if (it.hasConstructorParameter) { it.target.parameterIndex } else { Integer.MAX_VALUE } } return AdapterGenerator(type, sortedProperties) } } /** Writes this config to a [codeGenerator]. */ private fun ProguardConfig.writeTo(codeGenerator: CodeGenerator, originatingKSFile: KSFile) { val file = codeGenerator.createNewFile( dependencies = Dependencies(aggregating = false, originatingKSFile), packageName = "", fileName = outputFilePathWithoutExtension(targetClass.canonicalName), extensionName = "pro", ) // Don't use writeTo(file) because that tries to handle directories under the hood OutputStreamWriter(file, StandardCharsets.UTF_8).use(::writeTo) } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/KspUtil.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.ksp import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.ClassKind import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSNode import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSTypeAlias import com.google.devtools.ksp.symbol.Origin.KOTLIN import com.google.devtools.ksp.symbol.Origin.KOTLIN_LIB import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.CodeBlock import com.squareup.kotlinpoet.ksp.toClassName internal fun KSClassDeclaration.asType() = asType(emptyList()) internal fun KSClassDeclaration.isKotlinClass(): Boolean { return origin == KOTLIN || origin == KOTLIN_LIB || isAnnotationPresent(Metadata::class) } internal inline fun KSAnnotated.findAnnotationWithType(): T? { return getAnnotationsByType(T::class).firstOrNull() } internal fun KSType.unwrapTypeAlias(): KSType { return if (this.declaration is KSTypeAlias) { (this.declaration as KSTypeAlias).type.resolve() } else { this } } internal fun KSAnnotation.toAnnotationSpec(resolver: Resolver): AnnotationSpec { val element = annotationType.resolve().unwrapTypeAlias().declaration as KSClassDeclaration val builder = AnnotationSpec.builder(element.toClassName()) for (argument in arguments) { val member = CodeBlock.builder() val name = argument.name!!.getShortName() member.add("%L = ", name) addValueToBlock(argument.value!!, resolver, member, element) builder.addMember(member.build()) } return builder.build() } private fun addValueToBlock( value: Any, resolver: Resolver, member: CodeBlock.Builder, annotationContext: KSClassDeclaration? = null, ) { when (value) { is List<*> -> { // Array type member.add("arrayOf(⇥⇥") value.forEachIndexed { index, innerValue -> if (index > 0) member.add(", ") addValueToBlock(innerValue!!, resolver, member, annotationContext) } member.add("⇤⇤)") } is KSType -> { val unwrapped = value.unwrapTypeAlias() val isEnum = (unwrapped.declaration as KSClassDeclaration).classKind == ClassKind.ENUM_ENTRY if (isEnum) { val parent = unwrapped.declaration.parentDeclaration as KSClassDeclaration val entry = unwrapped.declaration.simpleName.getShortName() member.add("%T.%L", parent.toClassName(), entry) } else { member.add("%T::class", unwrapped.toClassName()) } } is KSClassDeclaration -> { // Handle enum entries that come directly as KSClassDeclaration if (value.classKind == ClassKind.ENUM_ENTRY) { val enumEntry = value.simpleName.getShortName() val parentClass = value.parentDeclaration as? KSClassDeclaration if (parentClass != null && parentClass.classKind == ClassKind.ENUM_CLASS) { member.add("%T.%L", parentClass.toClassName(), enumEntry) } else { member.add("%L", enumEntry) } } else { member.add("%T::class", value.toClassName()) } } is KSAnnotation -> member.add("%L", value.toAnnotationSpec(resolver)) else -> member.add(memberForValue(value)) } } /** * Creates a [CodeBlock] with parameter `format` depending on the given `value` object. Handles a * number of special cases, such as appending "f" to `Float` values, and uses `%L` for other types. */ internal fun memberForValue(value: Any) = when (value) { is Class<*> -> CodeBlock.of("%T::class", value) is Enum<*> -> CodeBlock.of("%T.%L", value.javaClass, value.name) is String -> CodeBlock.of("%S", value) is Float -> CodeBlock.of("%Lf", value) is Double -> CodeBlock.of("%L", value) is Char -> CodeBlock.of("$value.toChar()") is Byte -> CodeBlock.of("$value.toByte()") is Short -> CodeBlock.of("$value.toShort()") // Int or Boolean else -> CodeBlock.of("%L", value) } internal inline fun KSPLogger.check(condition: Boolean, message: () -> String) { check(condition, null, message) } internal inline fun KSPLogger.check(condition: Boolean, element: KSNode?, message: () -> String) { if (!condition) { error(message(), element) } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/MoshiApiUtil.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.ksp import com.google.devtools.ksp.getClassDeclarationByName import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSDeclaration import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.KModifier import com.squareup.moshi.JsonQualifier import com.squareup.moshi.kotlin.codegen.api.DelegateKey import com.squareup.moshi.kotlin.codegen.api.PropertyGenerator import com.squareup.moshi.kotlin.codegen.api.TargetProperty import com.squareup.moshi.kotlin.codegen.api.rawType private val TargetProperty.isSettable get() = propertySpec.mutable || parameter != null private val TargetProperty.isVisible: Boolean get() { return visibility == KModifier.INTERNAL || visibility == KModifier.PROTECTED || visibility == KModifier.PUBLIC } /** * Returns a generator for this property, or null if either there is an error and this property * cannot be used with code gen, or if no codegen is necessary for this property. */ internal fun TargetProperty.generator( logger: KSPLogger, resolver: Resolver, originalType: KSDeclaration, ): PropertyGenerator? { if (jsonIgnore) { if (!hasDefault) { logger.error("No default value for transient/ignored property $name", originalType) return null } return PropertyGenerator(this, DelegateKey(type, emptyList()), true) } if (!isVisible) { logger.error("property $name is not visible", originalType) return null } if (!isSettable) { return null // This property is not settable. Ignore it. } // Merge parameter and property annotations val qualifiers = parameter?.qualifiers.orEmpty() + propertySpec.annotations for (jsonQualifier in qualifiers) { val qualifierRawType = jsonQualifier.typeName.rawType() // Check Java types since that covers both Java and Kotlin annotations. resolver.getClassDeclarationByName(qualifierRawType.canonicalName)?.let { annotationElement -> annotationElement.findAnnotationWithType()?.let { if (it.value != AnnotationRetention.RUNTIME) { logger.error("JsonQualifier @${qualifierRawType.simpleName} must have RUNTIME retention") } } } } val jsonQualifierSpecs = qualifiers.map { it.toBuilder().useSiteTarget(AnnotationSpec.UseSiteTarget.FIELD).build() } return PropertyGenerator(this, DelegateKey(type, jsonQualifierSpecs)) } internal val KSClassDeclaration.isJsonQualifier: Boolean get() = isAnnotationPresent(JsonQualifier::class) ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/TargetTypes.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.ksp import com.google.devtools.ksp.KspExperimental import com.google.devtools.ksp.getDeclaredProperties import com.google.devtools.ksp.getVisibility import com.google.devtools.ksp.isInternal import com.google.devtools.ksp.isLocal import com.google.devtools.ksp.isPublic import com.google.devtools.ksp.processing.KSPLogger import com.google.devtools.ksp.processing.Resolver import com.google.devtools.ksp.symbol.ClassKind import com.google.devtools.ksp.symbol.ClassKind.CLASS import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSDeclaration import com.google.devtools.ksp.symbol.KSPropertyDeclaration import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSTypeAlias import com.google.devtools.ksp.symbol.KSTypeParameter import com.google.devtools.ksp.symbol.Modifier import com.google.devtools.ksp.symbol.Nullability import com.google.devtools.ksp.symbol.Origin import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.PropertySpec import com.squareup.kotlinpoet.TypeName import com.squareup.kotlinpoet.ksp.TypeParameterResolver import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toKModifier import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.toTypeParameterResolver import com.squareup.kotlinpoet.ksp.toTypeVariableName import com.squareup.moshi.Json import com.squareup.moshi.JsonQualifier import com.squareup.moshi.kotlin.codegen.api.TargetConstructor import com.squareup.moshi.kotlin.codegen.api.TargetParameter import com.squareup.moshi.kotlin.codegen.api.TargetProperty import com.squareup.moshi.kotlin.codegen.api.TargetType import com.squareup.moshi.kotlin.codegen.api.unwrapTypeAlias /** Returns a target type for [type] or null if it cannot be used with code gen. */ internal fun targetType( type: KSDeclaration, resolver: Resolver, logger: KSPLogger, isInline: Boolean = false, ): TargetType? { if (type !is KSClassDeclaration) { logger.error( "@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must be a Kotlin class", type, ) return null } logger.check(type.classKind != ClassKind.ENUM_CLASS, type) { "@JsonClass with 'generateAdapter = \"true\"' can't be applied to ${type.qualifiedName?.asString()}: code gen for enums is not supported or necessary" } logger.check(type.classKind == CLASS && type.origin == Origin.KOTLIN, type) { "@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must be a Kotlin class" } logger.check(Modifier.INNER !in type.modifiers, type) { "@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must not be an inner class" } logger.check(Modifier.SEALED !in type.modifiers, type) { "@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must not be sealed" } logger.check(Modifier.ABSTRACT !in type.modifiers, type) { "@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must not be abstract" } logger.check(!type.isLocal(), type) { "@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must not be local" } logger.check(type.isPublic() || type.isInternal(), type) { "@JsonClass can't be applied to ${type.qualifiedName?.asString()}: must be internal or public" } val classTypeParamsResolver = type.typeParameters.toTypeParameterResolver(sourceTypeHint = type.qualifiedName!!.asString()) val typeVariables = type.typeParameters.map { it.toTypeVariableName(classTypeParamsResolver) } val appliedType = AppliedType(type) val constructor = primaryConstructor(resolver, type, classTypeParamsResolver, logger) ?: run { logger.error("No primary constructor found on $type", type) return null } if (constructor.visibility != KModifier.INTERNAL && constructor.visibility != KModifier.PUBLIC) { logger.error( "@JsonClass can't be applied to $type: " + "primary constructor is not internal or public", type, ) return null } val properties = mutableMapOf() val originalType = appliedType.type for (superclass in appliedType.superclasses(resolver)) { val classDecl = superclass.type if (!classDecl.isKotlinClass()) { logger.error( """ @JsonClass can't be applied to $type: supertype $superclass is not a Kotlin type. Origin=${classDecl.origin} Annotations=${classDecl.annotations.joinToString(prefix = "[", postfix = "]") { it.shortName.getShortName() }} """ .trimIndent(), type, ) return null } val supertypeProperties = declaredProperties( constructor = constructor, originalType = originalType, classDecl = classDecl, resolver = resolver, typeParameterResolver = classDecl.typeParameters.toTypeParameterResolver(classTypeParamsResolver), ) for ((name, property) in supertypeProperties) { properties.putIfAbsent(name, property) } } val visibility = type.getVisibility().toKModifier() ?: KModifier.PUBLIC // If any class in the enclosing class hierarchy is internal, they must all have internal // generated adapters. val resolvedVisibility = if (visibility == KModifier.INTERNAL) { // Our nested type is already internal, no need to search visibility } else { // Implicitly public, so now look up the hierarchy val forceInternal = generateSequence(type) { it.parentDeclaration } .filterIsInstance() .any { it.isInternal() } if (forceInternal) KModifier.INTERNAL else visibility } return TargetType( typeName = type.toClassName().withTypeArguments(typeVariables), constructor = constructor, properties = properties, typeVariables = typeVariables, isDataClass = Modifier.DATA in type.modifiers, visibility = resolvedVisibility, isValueClass = Modifier.VALUE in type.modifiers, isInline = isInline, ) } private fun ClassName.withTypeArguments(arguments: List): TypeName { return if (arguments.isEmpty()) { this } else { this.parameterizedBy(arguments) } } @OptIn(KspExperimental::class) internal fun primaryConstructor( resolver: Resolver, targetType: KSClassDeclaration, typeParameterResolver: TypeParameterResolver, logger: KSPLogger, ): TargetConstructor? { val primaryConstructor = targetType.primaryConstructor ?: return null val parameters = LinkedHashMap() for ((index, parameter) in primaryConstructor.parameters.withIndex()) { val name = parameter.name!!.getShortName() parameters[name] = TargetParameter( name = name, index = index, type = parameter.type.toTypeName(typeParameterResolver), hasDefault = parameter.hasDefault, qualifiers = parameter.qualifiers(resolver), jsonName = parameter.jsonName(), ) } val kmConstructorSignature: String = resolver.mapToJvmSignature(primaryConstructor) ?: run { logger.error("No primary constructor found.", primaryConstructor) return null } return TargetConstructor( parameters, primaryConstructor.getVisibility().toKModifier() ?: KModifier.PUBLIC, kmConstructorSignature, ) } private fun KSAnnotated?.qualifiers(resolver: Resolver): Set { if (this == null) return setOf() return annotations .filter { it.annotationType.resolve().declaration.isAnnotationPresent(JsonQualifier::class) } .mapTo(mutableSetOf()) { it.toAnnotationSpec(resolver) } } private fun KSAnnotated?.jsonName(): String? { return this?.findAnnotationWithType()?.name?.takeUnless { it == Json.UNSET_NAME } } private fun KSAnnotated?.jsonIgnore(): Boolean { return this?.findAnnotationWithType()?.ignore ?: false } @OptIn(KspExperimental::class) private fun declaredProperties( constructor: TargetConstructor, originalType: KSClassDeclaration, classDecl: KSClassDeclaration, resolver: Resolver, typeParameterResolver: TypeParameterResolver, ): Map { val result = mutableMapOf() for (property in classDecl.getDeclaredProperties()) { val initialType = property.type.resolve() val resolvedType = if (initialType.declaration is KSTypeParameter) { property.asMemberOf(originalType.asType()) } else { initialType } val propertySpec = property.toPropertySpec(resolver, resolvedType, typeParameterResolver) val name = propertySpec.name val parameter = constructor.parameters[name] val isTransient = Modifier.JAVA_TRANSIENT in property.modifiers || property.isAnnotationPresent(Transient::class) || Modifier.JAVA_TRANSIENT in resolver.effectiveJavaModifiers(property) result[name] = TargetProperty( propertySpec = propertySpec, parameter = parameter, visibility = property.getVisibility().toKModifier() ?: KModifier.PUBLIC, jsonName = parameter?.jsonName ?: property.jsonName() ?: name, jsonIgnore = isTransient || parameter?.jsonIgnore == true || property.jsonIgnore(), ) } return result } private fun KSPropertyDeclaration.toPropertySpec( resolver: Resolver, resolvedType: KSType, typeParameterResolver: TypeParameterResolver, ): PropertySpec { return PropertySpec.builder( name = simpleName.getShortName(), type = resolvedType .toTypeName(typeParameterResolver) .unwrapTypeAlias() .fixTypeAliasNullability(resolvedType), ) .mutable(isMutable) .addModifiers(modifiers.map { KModifier.valueOf(it.name) }) .apply { addAnnotations( this@toPropertySpec.annotations .mapNotNull { val isJsonQualifier = (it.annotationType.resolve().unwrapTypeAlias().declaration as KSClassDeclaration) .isJsonQualifier if (isJsonQualifier) { it.toAnnotationSpec(resolver) } else { null } } .asIterable() ) } .build() } private fun TypeName.fixTypeAliasNullability(resolvedType: KSType): TypeName { return if (resolvedType.declaration is KSTypeAlias) { copy(nullable = resolvedType.nullability == Nullability.NULLABLE) } else { this } } ================================================ FILE: moshi-kotlin-codegen/src/main/java/com/squareup/moshi/kotlin/codegen/ksp/shadedUtil.kt ================================================ /* * Copyright 2020 Google LLC * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. * * 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.squareup.moshi.kotlin.codegen.ksp import com.google.devtools.ksp.symbol.KSAnnotated import com.google.devtools.ksp.symbol.KSAnnotation import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSDeclaration import com.google.devtools.ksp.symbol.KSType import com.google.devtools.ksp.symbol.KSValueArgument import java.lang.reflect.InvocationHandler import java.lang.reflect.Method import java.lang.reflect.Proxy import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass /* * Copied experimental utilities from KSP. */ internal fun KSAnnotated.getAnnotationsByType( annotationKClass: KClass ): Sequence { return this.annotations .filter { it.shortName.getShortName() == annotationKClass.simpleName && it.annotationType.resolve().declaration.qualifiedName?.asString() == annotationKClass.qualifiedName } .map { it.toAnnotation(annotationKClass.java) } } internal fun KSAnnotated.isAnnotationPresent( annotationKClass: KClass ): Boolean = getAnnotationsByType(annotationKClass).firstOrNull() != null @Suppress("UNCHECKED_CAST") private fun KSAnnotation.toAnnotation(annotationClass: Class): T { return Proxy.newProxyInstance( annotationClass.classLoader, arrayOf(annotationClass), createInvocationHandler(annotationClass), ) as T } @Suppress("TooGenericExceptionCaught") private fun KSAnnotation.createInvocationHandler(clazz: Class<*>): InvocationHandler { val cache = ConcurrentHashMap, Any>, Any>(arguments.size) return InvocationHandler { proxy, method, _ -> if (method.name == "toString" && arguments.none { it.name?.asString() == "toString" }) { clazz.canonicalName + arguments .map { argument: KSValueArgument -> // handles default values for enums otherwise returns null val methodName = argument.name?.asString() val value = proxy.javaClass.methods.find { m -> m.name == methodName }?.invoke(proxy) "$methodName=$value" } .toList() } else { val argument = arguments.first { it.name?.asString() == method.name } when (val result = argument.value ?: method.defaultValue) { is Proxy -> result is List<*> -> { val value = { result.asArray(method, clazz) } cache.getOrPut(Pair(method.returnType, result), value) } else -> { when { // Workaround for java annotation value array type // https://github.com/google/ksp/issues/1329 method.returnType.isArray -> { if (result !is Array<*>) { val value = { result.asArray(method, clazz) } cache.getOrPut(Pair(method.returnType, value), value) } else { throw IllegalStateException( "unhandled value type, please file a bug at https://github.com/google/ksp/issues/new" ) } } method.returnType.isEnum -> { val value = { result.asEnum(method.returnType) } cache.getOrPut(Pair(method.returnType, result), value) } method.returnType.isAnnotation -> { val value = { (result as KSAnnotation).asAnnotation(method.returnType) } cache.getOrPut(Pair(method.returnType, result), value) } method.returnType.name == "java.lang.Class" -> { cache.getOrPut(Pair(method.returnType, result)) { when (result) { is KSType -> result.asClass(clazz) // Handles com.intellij.psi.impl.source.PsiImmediateClassType using reflection // since api doesn't contain a reference to this else -> Class.forName( result.javaClass.methods .first { it.name == "getCanonicalText" } .invoke(result, false) as String ) } } } method.returnType.name == "byte" -> { val value = { result.asByte() } cache.getOrPut(Pair(method.returnType, result), value) } method.returnType.name == "short" -> { val value = { result.asShort() } cache.getOrPut(Pair(method.returnType, result), value) } method.returnType.name == "long" -> { val value = { result.asLong() } cache.getOrPut(Pair(method.returnType, result), value) } method.returnType.name == "float" -> { val value = { result.asFloat() } cache.getOrPut(Pair(method.returnType, result), value) } method.returnType.name == "double" -> { val value = { result.asDouble() } cache.getOrPut(Pair(method.returnType, result), value) } else -> result // original value } } } } } } @Suppress("UNCHECKED_CAST") private fun KSAnnotation.asAnnotation(annotationInterface: Class<*>): Any { return Proxy.newProxyInstance( this.javaClass.classLoader, arrayOf(annotationInterface), this.createInvocationHandler(annotationInterface), ) as Proxy } @Suppress("UNCHECKED_CAST") private fun List<*>.asArray(method: Method, proxyClass: Class<*>) = when (method.returnType.componentType.name) { "boolean" -> (this as List).toBooleanArray() "byte" -> (this as List).toByteArray() "short" -> (this as List).toShortArray() "char" -> (this as List).toCharArray() "double" -> (this as List).toDoubleArray() "float" -> (this as List).toFloatArray() "int" -> (this as List).toIntArray() "long" -> (this as List).toLongArray() "java.lang.Class" -> (this as List).asClasses(proxyClass).toTypedArray() "java.lang.String" -> (this as List).toTypedArray() else -> { // arrays of enums or annotations when { method.returnType.componentType.isEnum -> { this.toArray(method) { result -> result.asEnum(method.returnType.componentType) } } method.returnType.componentType.isAnnotation -> { this.toArray(method) { result -> (result as KSAnnotation).asAnnotation(method.returnType.componentType) } } else -> throw IllegalStateException( "Unable to process type ${method.returnType.componentType.name}" ) } } } @Suppress("UNCHECKED_CAST") private fun List<*>.toArray(method: Method, valueProvider: (Any) -> Any): Array { val array: Array = java.lang.reflect.Array.newInstance(method.returnType.componentType, this.size) as Array for (r in 0 until this.size) { array[r] = this[r]?.let { valueProvider.invoke(it) } } return array } @Suppress("UNCHECKED_CAST") private fun Any.asEnum(returnType: Class): T = returnType .getDeclaredMethod("valueOf", String::class.java) .invoke( null, if (this is KSType) { this.declaration.simpleName.getShortName() } else if (this is KSClassDeclaration) { this.simpleName.getShortName() } else { this.toString() }, ) as T private fun Any.asByte(): Byte = if (this is Int) this.toByte() else this as Byte private fun Any.asShort(): Short = if (this is Int) this.toShort() else this as Short private fun Any.asLong(): Long = if (this is Int) this.toLong() else this as Long private fun Any.asFloat(): Float = if (this is Int) this.toFloat() else this as Float private fun Any.asDouble(): Double = if (this is Int) this.toDouble() else this as Double // for Class/KClass member internal class KSTypeNotPresentException(val ksType: KSType, cause: Throwable) : RuntimeException(cause) // for Class[]/Array> member. internal class KSTypesNotPresentException(val ksTypes: List, cause: Throwable) : RuntimeException(cause) private fun KSType.asClass(proxyClass: Class<*>) = try { Class.forName(this.declaration.toJavaClassName(), true, proxyClass.classLoader) } catch (e: Exception) { throw KSTypeNotPresentException(this, e) } private fun List.asClasses(proxyClass: Class<*>) = try { this.map { type -> type.asClass(proxyClass) } } catch (e: Exception) { throw KSTypesNotPresentException(this, e) } private fun Any.asArray(method: Method, proxyClass: Class<*>) = listOf(this).asArray(method, proxyClass) private fun KSDeclaration.toJavaClassName(): String { val nameDelimiter = '.' val packageNameString = packageName.asString() val qualifiedNameString = qualifiedName!!.asString() val simpleNames = qualifiedNameString.removePrefix("${packageNameString}$nameDelimiter").split(nameDelimiter) return if (simpleNames.size > 1) { buildString { append(packageNameString) append(nameDelimiter) simpleNames.forEachIndexed { index, s -> if (index > 0) { append('$') } append(s) } } } else { qualifiedNameString } } ================================================ FILE: moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/JavaSuperclass.java ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen; import com.squareup.moshi.kotlin.codegen.ksp.JsonClassSymbolProcessorTest; /** For {@link JsonClassSymbolProcessorTest#extendJavaType}. */ public class JavaSuperclass { public int a = 1; } ================================================ FILE: moshi-kotlin-codegen/src/test/java/com/squareup/moshi/kotlin/codegen/ksp/JsonClassSymbolProcessorTest.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.ksp import com.google.common.truth.Truth.assertThat import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATED import com.squareup.moshi.kotlin.codegen.api.Options.OPTION_GENERATE_PROGUARD_RULES import com.tschuchort.compiletesting.JvmCompilationResult import com.tschuchort.compiletesting.KotlinCompilation import com.tschuchort.compiletesting.SourceFile import com.tschuchort.compiletesting.SourceFile.Companion.java import com.tschuchort.compiletesting.SourceFile.Companion.kotlin import com.tschuchort.compiletesting.configureKsp import com.tschuchort.compiletesting.kspProcessorOptions import com.tschuchort.compiletesting.kspSourcesDir import kotlin.reflect.KTypeProjection import kotlin.reflect.full.createType import kotlin.reflect.full.declaredMemberProperties import kotlin.reflect.typeOf import org.junit.Ignore import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder /** Execute kotlinc to confirm that either files are generated or errors are printed. */ class JsonClassSymbolProcessorTest { @Rule @JvmField val temporaryFolder: TemporaryFolder = TemporaryFolder() @Test fun privateConstructor() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class PrivateConstructor private constructor(var a: Int, var b: Int) { fun a() = a fun b() = b companion object { fun newInstance(a: Int, b: Int) = PrivateConstructor(a, b) } } """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages).contains("constructor is not internal or public") } @Test fun privateConstructorParameter() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class PrivateConstructorParameter(private var a: Int) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages).contains("property a is not visible") } @Test fun privateProperties() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class PrivateProperties { private var a: Int = -1 private var b: Int = -1 } """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages).contains("property a is not visible") } @Test fun interfacesNotSupported() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) interface Interface """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains("@JsonClass can't be applied to test.Interface: must be a Kotlin class") } @Test fun interfacesDoNotErrorWhenGeneratorNotSet() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true, generator="customGenerator") interface Interface """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) } @Test fun abstractClassesNotSupported() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) abstract class AbstractClass(val a: Int) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains("@JsonClass can't be applied to test.AbstractClass: must not be abstract") } @Test fun sealedClassesNotSupported() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) sealed class SealedClass(val a: Int) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains("@JsonClass can't be applied to test.SealedClass: must not be sealed") } @Test fun innerClassesNotSupported() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass class Outer { @JsonClass(generateAdapter = true) inner class InnerClass(val a: Int) } """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains("@JsonClass can't be applied to test.Outer.InnerClass: must not be an inner class") } @Test fun enumClassesNotSupported() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) enum class KotlinEnum { A, B } """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains( "@JsonClass with 'generateAdapter = \"true\"' can't be applied to test.KotlinEnum: code gen for enums is not supported or necessary" ) } // Annotation processors don't get called for local classes, so we don't have the opportunity to @Ignore @Test fun localClassesNotSupported() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass fun outer() { @JsonClass(generateAdapter = true) class LocalClass(val a: Int) } """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains("@JsonClass can't be applied to LocalClass: must not be local") } @Test fun privateClassesNotSupported() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) private class PrivateClass(val a: Int) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains("@JsonClass can't be applied to test.PrivateClass: must be internal or public") } @Test fun objectDeclarationsNotSupported() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) object ObjectDeclaration { var a = 5 } """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains("@JsonClass can't be applied to test.ObjectDeclaration: must be a Kotlin class") } @Test fun objectExpressionsNotSupported() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) val expression = object : Any() { var a = 5 } """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains("@JsonClass can't be applied to test.expression: must be a Kotlin class") } @Test fun requiredTransientConstructorParameterFails() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class RequiredTransientConstructorParameter(@Transient var a: Int) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages).contains("No default value for transient/ignored property a") } @Test fun requiredIgnoredConstructorParameterFails() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.Json import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class RequiredTransientConstructorParameter(@Json(ignore = true) var a: Int) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages).contains("No default value for transient/ignored property a") } @Test fun nonPropertyConstructorParameter() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class NonPropertyConstructorParameter(a: Int, val b: Int) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages).contains("No property for required constructor parameter a") } @Test fun badGeneratedAnnotation() { val result = prepareCompilation( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Foo(val a: Int) """, ) ) .apply { kspProcessorOptions[OPTION_GENERATED] = "javax.annotation.GeneratedBlerg" } .compile() assertThat(result.messages).contains("Invalid option value for $OPTION_GENERATED") } @Test fun disableProguardGeneration() { val compilation = prepareCompilation( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class Foo(val a: Int) """, ) ) .apply { kspProcessorOptions[OPTION_GENERATE_PROGUARD_RULES] = "false" } val result = compilation.compile() assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) assertThat(compilation.kspSourcesDir.walkTopDown().filter { it.extension == "pro" }.toList()) .isEmpty() } @Test fun multipleErrors() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) class Class1(private var a: Int, private var b: Int) @JsonClass(generateAdapter = true) class Class2(private var c: Int) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages).contains("property a is not visible") assertThat(result.messages).contains("property c is not visible") } @Test fun extendPlatformType() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass import java.util.Date @JsonClass(generateAdapter = true) class ExtendsPlatformClass(var a: Int) : Date() """, ) ) assertThat(result.messages).contains("supertype java.util.Date is not a Kotlin type") } @Test fun extendJavaType() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass import com.squareup.moshi.kotlin.codegen.JavaSuperclass @JsonClass(generateAdapter = true) class ExtendsJavaType(var b: Int) : JavaSuperclass() """, ), java( "JavaSuperclass.java", """ package com.squareup.moshi.kotlin.codegen; public class JavaSuperclass { public int a = 1; } """, ), ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains("supertype com.squareup.moshi.kotlin.codegen.JavaSuperclass is not a Kotlin type") } @Test fun nonFieldApplicableQualifier() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonQualifier import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.PROPERTY import kotlin.annotation.Retention import kotlin.annotation.Target @Retention(RUNTIME) @Target(PROPERTY) @JsonQualifier annotation class UpperCase @JsonClass(generateAdapter = true) class ClassWithQualifier(@UpperCase val a: Int) """, ) ) // We instantiate directly, no FIELD site target necessary assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) } @Test fun nonRuntimeQualifier() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonQualifier import kotlin.annotation.AnnotationRetention.BINARY import kotlin.annotation.AnnotationTarget.FIELD import kotlin.annotation.AnnotationTarget.PROPERTY import kotlin.annotation.Retention import kotlin.annotation.Target @Retention(BINARY) @Target(PROPERTY, FIELD) @JsonQualifier annotation class UpperCase @JsonClass(generateAdapter = true) class ClassWithQualifier(@UpperCase val a: Int) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages).contains("JsonQualifier @UpperCase must have RUNTIME retention") } @Test fun invalidGenericSyntaxErrorMessaging() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ElementEnvelope(val elements: List) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages).contains("Error preparing ElementEnvelope") } @Test fun inlineClassWithMultiplePropertiesFails() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true, inline = true) class MultipleProperties(val a: Int, val b: Int) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains( "@JsonClass with inline = true requires exactly one non-transient property, but " + "MultipleProperties has 2: a, b." ) } @Test fun inlineClassWithNullablePropertyFails() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true, inline = true) class NullableProperty(val a: Int?) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.COMPILATION_ERROR) assertThat(result.messages) .contains( "@JsonClass with inline = true requires a non-nullable property, " + "but NullableProperty.a is nullable." ) } @Test fun `TypeAliases with the same backing type should share the same adapter`() { val result = compile( kotlin( "source.kt", """ package test import com.squareup.moshi.JsonClass typealias FirstName = String typealias LastName = String @JsonClass(generateAdapter = true) data class Person(val firstName: FirstName, val lastName: LastName, val hairColor: String) """, ) ) assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) // We're checking here that we only generate one `stringAdapter` that's used for both the // regular string properties as well as the aliased ones. val adapterClass = result.classLoader.loadClass("test.PersonJsonAdapter").kotlin assertThat(adapterClass.declaredMemberProperties.map { it.returnType }) .containsExactly( JsonReader.Options::class.createType(), JsonAdapter::class.createType(listOf(KTypeProjection.invariant(typeOf()))), ) } @Test fun `Processor should generate comprehensive proguard rules`() { val compilation = prepareCompilation( kotlin( "source.kt", """ package testPackage import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonQualifier typealias FirstName = String typealias LastName = String @JsonClass(generateAdapter = true) data class Aliases(val firstName: FirstName, val lastName: LastName, val hairColor: String) @JsonClass(generateAdapter = true) data class Simple(val firstName: String) @JsonClass(generateAdapter = true) data class Generic(val firstName: T, val lastName: String) @JsonQualifier annotation class MyQualifier @JsonClass(generateAdapter = true) data class UsingQualifiers(val firstName: String, @MyQualifier val lastName: String) @JsonClass(generateAdapter = true) data class MixedTypes(val firstName: String, val otherNames: MutableList) @JsonClass(generateAdapter = true) data class DefaultParams(val firstName: String = "") @JsonClass(generateAdapter = true) data class Complex(val firstName: FirstName = "", @MyQualifier val names: MutableList, val genericProp: T) object NestedType { @JsonQualifier annotation class NestedQualifier @JsonClass(generateAdapter = true) data class NestedSimple(@NestedQualifier val firstName: String) } @JsonClass(generateAdapter = true) class MultipleMasks( val arg0: Long = 0, val arg1: Long = 1, val arg2: Long = 2, val arg3: Long = 3, val arg4: Long = 4, val arg5: Long = 5, val arg6: Long = 6, val arg7: Long = 7, val arg8: Long = 8, val arg9: Long = 9, val arg10: Long = 10, val arg11: Long, val arg12: Long = 12, val arg13: Long = 13, val arg14: Long = 14, val arg15: Long = 15, val arg16: Long = 16, val arg17: Long = 17, val arg18: Long = 18, val arg19: Long = 19, @Suppress("UNUSED_PARAMETER") arg20: Long = 20, val arg21: Long = 21, val arg22: Long = 22, val arg23: Long = 23, val arg24: Long = 24, val arg25: Long = 25, val arg26: Long = 26, val arg27: Long = 27, val arg28: Long = 28, val arg29: Long = 29, val arg30: Long = 30, val arg31: Long = 31, val arg32: Long = 32, val arg33: Long = 33, val arg34: Long = 34, val arg35: Long = 35, val arg36: Long = 36, val arg37: Long = 37, val arg38: Long = 38, @Transient val arg39: Long = 39, val arg40: Long = 40, val arg41: Long = 41, val arg42: Long = 42, val arg43: Long = 43, val arg44: Long = 44, val arg45: Long = 45, val arg46: Long = 46, val arg47: Long = 47, val arg48: Long = 48, val arg49: Long = 49, val arg50: Long = 50, val arg51: Long = 51, val arg52: Long = 52, @Transient val arg53: Long = 53, val arg54: Long = 54, val arg55: Long = 55, val arg56: Long = 56, val arg57: Long = 57, val arg58: Long = 58, val arg59: Long = 59, val arg60: Long = 60, val arg61: Long = 61, val arg62: Long = 62, val arg63: Long = 63, val arg64: Long = 64, val arg65: Long = 65 ) """, ) ) val result = compilation.compile() assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) compilation.kspSourcesDir .walkTopDown() .filter { it.extension == "pro" } .forEach { generatedFile -> when (generatedFile.nameWithoutExtension) { "moshi-testPackage.Aliases" -> assertThat(generatedFile.readText()) .contains( """ -keepnames class testPackage.Aliases -if class testPackage.Aliases -keep class testPackage.AliasesJsonAdapter { public (com.squareup.moshi.Moshi); } """ .trimIndent() ) "moshi-testPackage.Simple" -> assertThat(generatedFile.readText()) .contains( """ -keepnames class testPackage.Simple -if class testPackage.Simple -keep class testPackage.SimpleJsonAdapter { public (com.squareup.moshi.Moshi); } """ .trimIndent() ) "moshi-testPackage.Generic" -> assertThat(generatedFile.readText()) .contains( """ -keepnames class testPackage.Generic -if class testPackage.Generic -keep class testPackage.GenericJsonAdapter { public (com.squareup.moshi.Moshi,java.lang.reflect.Type[]); } """ .trimIndent() ) "moshi-testPackage.UsingQualifiers" -> { assertThat(generatedFile.readText()) .contains( """ -keepnames class testPackage.UsingQualifiers -if class testPackage.UsingQualifiers -keep class testPackage.UsingQualifiersJsonAdapter { public (com.squareup.moshi.Moshi); } """ .trimIndent() ) } "moshi-testPackage.MixedTypes" -> assertThat(generatedFile.readText()) .contains( """ -keepnames class testPackage.MixedTypes -if class testPackage.MixedTypes -keep class testPackage.MixedTypesJsonAdapter { public (com.squareup.moshi.Moshi); } """ .trimIndent() ) "moshi-testPackage.DefaultParams" -> assertThat(generatedFile.readText()) .contains( """ -keepnames class testPackage.DefaultParams -if class testPackage.DefaultParams -keep class testPackage.DefaultParamsJsonAdapter { public (com.squareup.moshi.Moshi); } -if class testPackage.DefaultParams -keepnames class kotlin.jvm.internal.DefaultConstructorMarker -keepclassmembers class testPackage.DefaultParams { public synthetic (java.lang.String,int,kotlin.jvm.internal.DefaultConstructorMarker); } """ .trimIndent() ) "moshi-testPackage.Complex" -> { assertThat(generatedFile.readText()) .contains( """ -keepnames class testPackage.Complex -if class testPackage.Complex -keep class testPackage.ComplexJsonAdapter { public (com.squareup.moshi.Moshi,java.lang.reflect.Type[]); } -if class testPackage.Complex -keepnames class kotlin.jvm.internal.DefaultConstructorMarker -keepclassmembers class testPackage.Complex { public synthetic (java.lang.String,java.util.List,java.lang.Object,int,kotlin.jvm.internal.DefaultConstructorMarker); } """ .trimIndent() ) } "moshi-testPackage.MultipleMasks" -> assertThat(generatedFile.readText()) .contains( """ -keepnames class testPackage.MultipleMasks -if class testPackage.MultipleMasks -keep class testPackage.MultipleMasksJsonAdapter { public (com.squareup.moshi.Moshi); } -if class testPackage.MultipleMasks -keepnames class kotlin.jvm.internal.DefaultConstructorMarker -keepclassmembers class testPackage.MultipleMasks { public synthetic (long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,long,int,int,int,kotlin.jvm.internal.DefaultConstructorMarker); } """ .trimIndent() ) "moshi-testPackage.NestedType.NestedSimple" -> { assertThat(generatedFile.readText()) .contains( """ -keepnames class testPackage.NestedType${'$'}NestedSimple -if class testPackage.NestedType${'$'}NestedSimple -keep class testPackage.NestedType_NestedSimpleJsonAdapter { public (com.squareup.moshi.Moshi); } """ .trimIndent() ) } else -> error("Unexpected proguard file! ${generatedFile.name}") } } } private fun prepareCompilation(vararg sourceFiles: SourceFile): KotlinCompilation { return KotlinCompilation().apply { workingDir = temporaryFolder.root inheritClassPath = true sources = sourceFiles.asList() verbose = false configureKsp { symbolProcessorProviders += JsonClassSymbolProcessorProvider() } } } private fun compile(vararg sourceFiles: SourceFile): JvmCompilationResult { return prepareCompilation(*sourceFiles).compile() } } ================================================ FILE: moshi-kotlin-tests/build.gradle.kts ================================================ import TestMode.KSP import TestMode.REFLECT import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") id("com.google.devtools.ksp") apply false } enum class TestMode { REFLECT, KSP, } val testMode = findProperty("kotlinTestMode")?.toString()?.let(TestMode::valueOf) ?: REFLECT when (testMode) { REFLECT -> { // Do nothing! } KSP -> { apply(plugin = "com.google.devtools.ksp") } } tasks.withType().configureEach { // ExtendsPlatformClassWithProtectedField tests a case where we set a protected // ByteArrayOutputStream.buf field jvmArgs("--add-opens=java.base/java.io=ALL-UNNAMED") } tasks.withType().configureEach { compilerOptions { allWarningsAsErrors.set(true) freeCompilerArgs.addAll( "-opt-in=kotlin.ExperimentalStdlibApi", "-Xannotation-default-target=param-property", ) } } dependencies { when (testMode) { REFLECT -> { // Do nothing } KSP -> { "kspTest"(project(":moshi-kotlin-codegen")) } } testImplementation(project(":moshi")) testImplementation(project(":moshi-kotlin")) testImplementation(project(":moshi-kotlin-tests:extra-moshi-test-module")) testImplementation(kotlin("reflect")) testImplementation(libs.junit) testImplementation(libs.assertj) testImplementation(libs.truth) } ================================================ FILE: moshi-kotlin-tests/codegen-only/build.gradle.kts ================================================ import TestMode.KSP import TestMode.REFLECT import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { kotlin("jvm") id("com.google.devtools.ksp") apply false } enum class TestMode { REFLECT, KSP, } val testMode = findProperty("kotlinTestMode")?.toString()?.let(TestMode::valueOf) ?: KSP when (testMode) { REFLECT -> { // Default to KSP. This is a CI-only thing apply(plugin = "com.google.devtools.ksp") } KSP -> { apply(plugin = "com.google.devtools.ksp") } } tasks.withType().configureEach { // ExtendsPlatformClassWithProtectedField tests a case where we set a protected // ByteArrayOutputStream.buf field jvmArgs("--add-opens=java.base/java.io=ALL-UNNAMED") } tasks.withType().configureEach { compilerOptions { allWarningsAsErrors.set(true) freeCompilerArgs.addAll( "-opt-in=kotlin.ExperimentalStdlibApi", "-Xannotation-default-target=param-property", ) } } dependencies { when (testMode) { REFLECT -> { // Default to KSP in this case, this is a CI-only thing "kspTest"(project(":moshi-kotlin-codegen")) } KSP -> { "kspTest"(project(":moshi-kotlin-codegen")) } } testImplementation(project(":moshi")) testImplementation(project(":moshi-kotlin")) testImplementation(project(":moshi-kotlin-tests:extra-moshi-test-module")) testImplementation(kotlin("reflect")) testImplementation(libs.junit) testImplementation(libs.assertj) testImplementation(libs.truth) } ================================================ FILE: moshi-kotlin-tests/codegen-only/src/test/kotlin/com/squareup/moshi/kotlin/codegen/CompileOnlyTests.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonQualifier import com.squareup.moshi.kotlin.codegen.test.extra.AbstractClassInModuleA import kotlin.annotation.AnnotationRetention.RUNTIME import kotlin.annotation.AnnotationTarget.TYPE /* * These are classes that need only compile. */ // Regression test for https://github.com/square/moshi/issues/905 @JsonClass(generateAdapter = true) data class GenericTestClassWithDefaults(val input: String = "", val genericInput: T) @Target(TYPE) annotation class TypeAnnotation /** * Compilation-only test to ensure we don't render types with their annotations. Regression test for * https://github.com/square/moshi/issues/1033 */ @JsonClass(generateAdapter = true) data class TypeAnnotationClass( val propertyWithAnnotatedType: @TypeAnnotation String = "", val generic: List<@TypeAnnotation String>, ) // Regression test for https://github.com/square/moshi/issues/1277 @JsonClass(generateAdapter = true) data class OtherTestModel(val TestModel: TestModel? = null) @JsonClass(generateAdapter = true) data class TestModel(val someVariable: Int, val anotherVariable: String) // Regression test for https://github.com/square/moshi/issues/1022 @JsonClass(generateAdapter = true) internal data class MismatchParentAndNestedClassVisibility( val type: Int, val name: String? = null, ) { @JsonClass(generateAdapter = true) data class NestedClass(val nestedProperty: String) } // Regression test for https://github.com/square/moshi/issues/1052 @JsonClass(generateAdapter = true) data class KeysWithSpaces( @Json(name = "1. Information") val information: String, @Json(name = "2. Symbol") val symbol: String, @Json(name = "3. Last Refreshed") val lastRefreshed: String, @Json(name = "4. Interval") val interval: String, @Json(name = "5. Output Size") val size: String, @Json(name = "6. Time Zone") val timeZone: String, ) // Regression test for https://github.com/square/moshi/issues/848 @JsonClass(generateAdapter = true) data class Hotwords(val `class`: List?) /** * This is here mostly just to ensure it still compiles. Covers variance, @Json, default values, * nullability, primitive arrays, and some wacky generics. */ @JsonClass(generateAdapter = true) data class SmokeTestType( @Json(name = "first_name") val firstName: String, @Json(name = "last_name") val lastName: String, val age: Int, val nationalities: List = emptyList(), val weight: Float, val tattoos: Boolean = false, val race: String?, val hasChildren: Boolean = false, val favoriteFood: String? = null, val favoriteDrink: String? = "Water", val wildcardOut: MutableList = mutableListOf(), val nullableWildcardOut: MutableList = mutableListOf(), val wildcardIn: Array, val any: List<*>, val anyTwo: List, val anyOut: MutableList, val nullableAnyOut: MutableList, val favoriteThreeNumbers: IntArray, val favoriteArrayValues: Array, val favoriteNullableArrayValues: Array, val nullableSetListMapArrayNullableIntWithDefault: Set>>>? = null, val aliasedName: TypeAliasName = "Woah", val genericAlias: GenericTypeAlias = listOf("Woah"), // Regression test for https://github.com/square/moshi/issues/1272 val nestedArray: Array>? = null, ) typealias TypeAliasName = String typealias GenericTypeAlias = List // Regression test for enum constants in annotations and array types // https://github.com/ZacSweers/MoshiX/issues/103 @Retention(RUNTIME) @JsonQualifier annotation class UpperCase(val foo: Array) enum class Foo { BAR } @JsonClass(generateAdapter = true) data class ClassWithQualifier(@UpperCase(foo = [Foo.BAR]) val a: Int) // Regression for https://github.com/ZacSweers/MoshiX/issues/120 @JsonClass(generateAdapter = true) data class DataClassInModuleB(val id: String) : AbstractClassInModuleA() ================================================ FILE: moshi-kotlin-tests/codegen-only/src/test/kotlin/com/squareup/moshi/kotlin/codegen/ComplexGenericsInheritanceTest.kt ================================================ /* * Copyright (C) 2020 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 * * https://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:Suppress("UNUSED", "UNUSED_PARAMETER") package com.squareup.moshi.kotlin.codegen import com.google.common.truth.Truth.assertThat import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import org.intellij.lang.annotations.Language import org.junit.Test class ComplexGenericsInheritanceTest { private val moshi = Moshi.Builder().build() @Test fun simple() { val adapter = moshi.adapter() @Language("JSON") val json = """{"data":{"name":"foo"},"data2":"bar","data3":"baz"}""" val instance = adapter.fromJson(json) val testInstance = PersonResponse().apply { data = Person("foo") } assertThat(instance).isEqualTo(testInstance) assertThat(adapter.toJson(instance)).isEqualTo(json) } @Test fun nested() { val adapter = moshi.adapter() @Language("JSON") val json = """{"data":{"name":"foo"},"data2":"bar","data3":"baz"}""" val instance = adapter.fromJson(json) val testInstance = NestedPersonResponse().apply { data = Person("foo") } assertThat(instance).isEqualTo(testInstance) assertThat(adapter.toJson(instance)).isEqualTo(json) } @Test fun untyped() { val adapter = moshi.adapter>() @Language("JSON") val json = """{"data":{"name":"foo"},"data2":"bar","data3":"baz"}""" val instance = adapter.fromJson(json) val testInstance = UntypedNestedPersonResponse().apply { data = Person("foo") } assertThat(instance).isEqualTo(testInstance) assertThat(adapter.toJson(instance)).isEqualTo(json) } @Test fun complex() { val adapter = moshi.adapter>>() @Language("JSON") val json = """{"layer4E":{"name":"layer4E"},"layer4F":{"data":{"name":"layer4F"},"data2":"layer4F","data3":"layer4F"},"layer3C":[1,2,3],"layer3D":"layer3D","layer2":"layer2","layer1":"layer1"}""" val instance = adapter.fromJson(json) val testInstance = Layer4( layer4E = Person("layer4E"), layer4F = UntypedNestedPersonResponse().apply { data = Person("layer4F") data2 = "layer4F" data3 = "layer4F" }, ) .apply { layer3C = listOf(1, 2, 3) layer3D = "layer3D" layer2 = "layer2" layer1 = "layer1" } assertThat(instance).isEqualTo(testInstance) assertThat(adapter.toJson(testInstance)).isEqualTo(json) } } open class ResponseWithSettableProperty { var data: T? = null var data2: R? = null var data3: R? = null } interface Personable @JsonClass(generateAdapter = true) data class Person(val name: String) : Personable @JsonClass(generateAdapter = true) data class PersonResponse(val extra: String? = null) : ResponseWithSettableProperty() abstract class NestedResponse : ResponseWithSettableProperty() @JsonClass(generateAdapter = true) data class NestedPersonResponse(val extra: String? = null) : NestedResponse() @JsonClass(generateAdapter = true) data class UntypedNestedPersonResponse(val extra: String? = null) : NestedResponse() interface LayerInterface abstract class Layer1 { var layer1: A? = null } abstract class Layer2 : Layer1(), LayerInterface { var layer2: B? = null } abstract class Layer3 : Layer2() { var layer3C: C? = null var layer3D: D? = null } @JsonClass(generateAdapter = true) data class Layer4(val layer4E: E, val layer4F: F? = null) : Layer3, String>(), LayerInterface ================================================ FILE: moshi-kotlin-tests/codegen-only/src/test/kotlin/com/squareup/moshi/kotlin/codegen/DefaultConstructorTest.kt ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi.kotlin.codegen import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import org.junit.Test class DefaultConstructorTest { @Test fun minimal() { val expected = TestClass("requiredClass") val json = """{"required":"requiredClass"}""" val instance = Moshi.Builder().build().adapter().fromJson(json) check(instance == expected) { "No match:\nActual : $instance\nExpected: $expected" } } @Test fun allSet() { val expected = TestClass("requiredClass", "customOptional", 4, "setDynamic", 5, 6) val json = """{"required":"requiredClass","optional":"customOptional","optional2":4,"dynamicSelfReferenceOptional":"setDynamic","dynamicOptional":5,"dynamicInlineOptional":6}""" val instance = Moshi.Builder().build().adapter().fromJson(json) check(instance == expected) { "No match:\nActual : $instance\nExpected: $expected" } } @Test fun customDynamic() { val expected = TestClass("requiredClass", "customOptional") val json = """{"required":"requiredClass","optional":"customOptional"}""" val instance = Moshi.Builder().build().adapter().fromJson(json) check(instance == expected) { "No match:\nActual : $instance\nExpected: $expected" } } } @JsonClass(generateAdapter = true) data class TestClass( val required: String, val optional: String = "optional", val optional2: Int = 2, val dynamicSelfReferenceOptional: String = required, val dynamicOptional: Int = createInt(), val dynamicInlineOptional: Int = createInlineInt(), ) private fun createInt(): Int { return 3 } @Suppress("NOTHING_TO_INLINE") private inline fun createInlineInt(): Int { return 3 } ================================================ FILE: moshi-kotlin-tests/codegen-only/src/test/kotlin/com/squareup/moshi/kotlin/codegen/GeneratedAdaptersTest.kt ================================================ /* * Copyright (C) 2018 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 * * https://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.squareup.moshi.kotlin.codegen import com.google.common.truth.Truth.assertThat import com.squareup.moshi.FromJson import com.squareup.moshi.Json import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonQualifier import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.ToJson import com.squareup.moshi.internal.NullSafeJsonAdapter import com.squareup.moshi.kotlin.codegen.annotation.UppercaseInAnnotationPackage import com.squareup.moshi.kotlin.codegen.annotation.UppercaseInAnnotationPackageJsonAdapter import java.util.Locale import kotlin.properties.Delegates import kotlin.reflect.full.memberProperties import org.intellij.lang.annotations.Language import org.junit.Assert.assertNull import org.junit.Assert.fail import org.junit.Ignore import org.junit.Test @Suppress("UNUSED", "UNUSED_PARAMETER") class GeneratedAdaptersTest { private val moshi = Moshi.Builder().build() @Test fun jsonAnnotation() { val adapter = moshi.adapter() // Read @Language("JSON") val json = """{"foo": "bar"}""" val instance = adapter.fromJson(json) assertThat(instance.bar).isEqualTo("bar") // Write @Language("JSON") val expectedJson = """{"foo":"baz"}""" assertThat(adapter.toJson(JsonAnnotation("baz"))).isEqualTo(expectedJson) } @JsonClass(generateAdapter = true) data class JsonAnnotation(@Json(name = "foo") val bar: String) @Test fun jsonAnnotationWithDollarSign() { val adapter = moshi.adapter() // Read val json = $$"{\"$foo\": \"bar\"}" val instance = adapter.fromJson(json) assertThat(instance.bar).isEqualTo("bar") // Write val expectedJson = "{\"\$foo\":\"baz\"}" assertThat(adapter.toJson(JsonAnnotationWithDollarSign("baz"))).isEqualTo(expectedJson) } @JsonClass(generateAdapter = true) data class JsonAnnotationWithDollarSign(@Json(name = "\$foo") val bar: String) @Test fun jsonAnnotationWithQuotationMark() { val adapter = moshi.adapter() // Read val json = """{"\"foo\"": "bar"}""" val instance = adapter.fromJson(json) assertThat(instance.bar).isEqualTo("bar") // Write val expectedJson = """{"\"foo\"":"baz"}""" assertThat(adapter.toJson(JsonAnnotationWithQuotationMark("baz"))).isEqualTo(expectedJson) } @JsonClass(generateAdapter = true) data class JsonAnnotationWithQuotationMark(@Json(name = "\"foo\"") val bar: String) @Test fun defaultValues() { val adapter = moshi.adapter() // Read/write with default values @Language("JSON") val json = """{"foo":"fooString"}""" val instance = adapter.fromJson(json) assertThat(instance.foo).isEqualTo("fooString") assertThat(instance.bar).isEqualTo("") assertThat(instance.nullableBar).isNull() assertThat(instance.bazList).apply { isNotNull() isEmpty() } @Language("JSON") val expected = """{"foo":"fooString","bar":"","bazList":[]}""" assertThat(adapter.toJson(DefaultValues("fooString"))).isEqualTo(expected) // Read/write with real values @Language("JSON") val json2 = """ {"foo":"fooString","bar":"barString","nullableBar":"bar","bazList":["baz"]} """ .trimIndent() val instance2 = adapter.fromJson(json2) assertThat(instance2.foo).isEqualTo("fooString") assertThat(instance2.bar).isEqualTo("barString") assertThat(instance2.nullableBar).isEqualTo("bar") assertThat(instance2.bazList).containsExactly("baz") assertThat(adapter.toJson(instance2)).isEqualTo(json2) } @JsonClass(generateAdapter = true) data class DefaultValues( val foo: String, val bar: String = "", val nullableBar: String? = null, val bazList: List = emptyList(), ) @Test fun nullableArray() { val adapter = moshi.adapter() @Language("JSON") val json = """{"data":[null,"why"]}""" val instance = adapter.fromJson(json) assertThat(instance.data).asList().containsExactly(null, "why").inOrder() assertThat(adapter.toJson(instance)).isEqualTo(json) } @JsonClass(generateAdapter = true) data class NullableArray(val data: Array) @Test fun primitiveArray() { val adapter = moshi.adapter() @Language("JSON") val json = """{"ints":[0,1]}""" val instance = adapter.fromJson(json) assertThat(instance.ints).asList().containsExactly(0, 1).inOrder() assertThat(adapter.toJson(instance)).isEqualTo(json) } @JsonClass(generateAdapter = true) data class PrimitiveArray(val ints: IntArray) @Test fun nullableTypes() { val adapter = moshi.adapter() @Language("JSON") val json = """{"foo":"foo","nullableString":null}""" @Language("JSON") val invalidJson = """{"foo":null,"nullableString":null}""" val instance = adapter.fromJson(json) assertThat(instance.foo).isEqualTo("foo") assertThat(instance.nullableString).isNull() try { adapter.fromJson(invalidJson) fail("The invalid json should have failed!") } catch (e: JsonDataException) { assertThat(e).hasMessageThat().contains("foo") } } @JsonClass(generateAdapter = true) data class NullabeTypes(val foo: String, val nullableString: String?) @Test fun collections() { val adapter = moshi.adapter() val specialCollections = SpecialCollections( mutableListOf(), mutableSetOf(), mutableMapOf(), emptyList(), emptySet(), emptyMap(), ) val json = adapter.toJson(specialCollections) val newCollections = adapter.fromJson(json) assertThat(newCollections).isEqualTo(specialCollections) } @JsonClass(generateAdapter = true) data class SpecialCollections( val mutableList: MutableList, val mutableSet: MutableSet, val mutableMap: MutableMap, val immutableList: List, val immutableSet: Set, val immutableMap: Map, ) @Test fun mutableProperties() { val adapter = moshi.adapter() val mutableProperties = MutableProperties( "immutableProperty", "mutableProperty", mutableListOf("immutableMutableList"), mutableListOf("immutableImmutableList"), mutableListOf("mutableMutableList"), mutableListOf("mutableImmutableList"), "immutableProperty", "mutableProperty", mutableListOf("immutableMutableList"), mutableListOf("immutableImmutableList"), mutableListOf("mutableMutableList"), mutableListOf("mutableImmutableList"), ) val json = adapter.toJson(mutableProperties) val newMutableProperties = adapter.fromJson(json) assertThat(newMutableProperties).isEqualTo(mutableProperties) } @JsonClass(generateAdapter = true) data class MutableProperties( val immutableProperty: String, var mutableProperty: String, val immutableMutableList: MutableList, val immutableImmutableList: List, var mutableMutableList: MutableList, var mutableImmutableList: List, val nullableImmutableProperty: String?, var nullableMutableProperty: String?, val nullableImmutableMutableList: MutableList?, val nullableImmutableImmutableList: List?, var nullableMutableMutableList: MutableList?, var nullableMutableImmutableList: List, ) @Test fun nullableTypeParams() { val adapter = moshi.adapter>() val nullSerializing = adapter.serializeNulls() val nullableTypeParams = NullableTypeParams( listOf("foo", null, "bar"), setOf("foo", null, "bar"), mapOf("foo" to "bar", "baz" to null), null, 1, ) val noNullsTypeParams = NullableTypeParams( nullableTypeParams.nullableList, nullableTypeParams.nullableSet, nullableTypeParams.nullableMap.filterValues { it != null }, null, 1, ) val json = adapter.toJson(nullableTypeParams) val newNullableTypeParams = adapter.fromJson(json) assertThat(newNullableTypeParams).isEqualTo(noNullsTypeParams) val nullSerializedJson = nullSerializing.toJson(nullableTypeParams) val nullSerializedNullableTypeParams = adapter.fromJson(nullSerializedJson) assertThat(nullSerializedNullableTypeParams).isEqualTo(nullableTypeParams) } @JsonClass(generateAdapter = true) data class NullableTypeParams( val nullableList: List, val nullableSet: Set, val nullableMap: Map, val nullableT: T?, val nonNullT: T, ) @Test fun doNotGenerateAdapter() { try { Class.forName("${GeneratedAdaptersTest::class.java.name}_DoNotGenerateAdapterJsonAdapter") fail("found a generated adapter for a type that shouldn't have one") } catch (expected: ClassNotFoundException) {} } @JsonClass(generateAdapter = false) data class DoNotGenerateAdapter(val foo: String) @Test fun constructorParameters() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = ConstructorParameters(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class ConstructorParameters(var a: Int, var b: Int) @Test fun properties() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = Properties() encoded.a = 3 encoded.b = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""") assertThat(decoded.a).isEqualTo(3) assertThat(decoded.b).isEqualTo(5) } @JsonClass(generateAdapter = true) class Properties { var a: Int = -1 var b: Int = -1 } @Test fun constructorParametersAndProperties() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = ConstructorParametersAndProperties(3) encoded.b = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class ConstructorParametersAndProperties(var a: Int) { var b: Int = -1 } @Test fun immutableConstructorParameters() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = ImmutableConstructorParameters(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class ImmutableConstructorParameters(val a: Int, val b: Int) @Test fun immutableProperties() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = ImmutableProperties(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""") assertThat(decoded.a).isEqualTo(3) assertThat(decoded.b).isEqualTo(5) } @JsonClass(generateAdapter = true) class ImmutableProperties(a: Int, b: Int) { val a = a val b = b } @Test fun constructorDefaults() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = ConstructorDefaultValues(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"b":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class ConstructorDefaultValues(var a: Int = -1, var b: Int = -2) @Test fun explicitNull() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = ExplicitNull(null, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":null,"b":6}""") assertThat(decoded.a).isEqualTo(null) assertThat(decoded.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class ExplicitNull(var a: Int?, var b: Int?) @Test fun absentNull() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = AbsentNull(null, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""") val decoded = jsonAdapter.fromJson("""{"b":6}""") assertThat(decoded.a).isNull() assertThat(decoded.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class AbsentNull(var a: Int?, var b: Int?) @Test fun constructorParameterWithQualifier() { val moshi = Moshi.Builder().add(UppercaseJsonAdapter()).build() val jsonAdapter = moshi.adapter() val encoded = ConstructorParameterWithQualifier("Android", "Banana") assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""") val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""") assertThat(decoded.a).isEqualTo("android") assertThat(decoded.b).isEqualTo("Banana") } @JsonClass(generateAdapter = true) class ConstructorParameterWithQualifier(@Uppercase(inFrench = true) var a: String, var b: String) @Test fun constructorParameterWithQualifierInAnnotationPackage() { val moshi = Moshi.Builder().add(UppercaseInAnnotationPackageJsonAdapter()).build() val jsonAdapter = moshi.adapter() val encoded = ConstructorParameterWithQualifierInAnnotationPackage("Android") assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID"}""") val decoded = jsonAdapter.fromJson("""{"a":"Android"}""") assertThat(decoded.a).isEqualTo("android") } @JsonClass(generateAdapter = true) class ConstructorParameterWithQualifierInAnnotationPackage( @UppercaseInAnnotationPackage var a: String ) @Test fun propertyWithQualifier() { val moshi = Moshi.Builder().add(UppercaseJsonAdapter()).build() val jsonAdapter = moshi.adapter() val encoded = PropertyWithQualifier() encoded.a = "Android" encoded.b = "Banana" assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""") val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""") assertThat(decoded.a).isEqualTo("android") assertThat(decoded.b).isEqualTo("Banana") } @JsonClass(generateAdapter = true) class PropertyWithQualifier { @Uppercase(inFrench = true) var a: String = "" var b: String = "" } @Test fun constructorParameterWithJsonName() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = ConstructorParameterWithJsonName(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class ConstructorParameterWithJsonName(@Json(name = "key a") var a: Int, var b: Int) @Test fun propertyWithJsonName() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = PropertyWithJsonName() encoded.a = 3 encoded.b = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class PropertyWithJsonName { @Json(name = "key a") var a: Int = -1 var b: Int = -1 } @Test fun transientConstructorParameter() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = TransientConstructorParameter(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class TransientConstructorParameter(@Transient var a: Int = -1, var b: Int = -1) @Test fun multipleTransientConstructorParameters() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = MultipleTransientConstructorParameters(3, 5, 7) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.b).isEqualTo(6) assertThat(decoded.c).isEqualTo(-1) } @JsonClass(generateAdapter = true) class MultipleTransientConstructorParameters( @Transient var a: Int = -1, var b: Int = -1, @Transient var c: Int = -1, ) @Test fun transientProperty() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = TransientProperty() encoded.a = 3 encoded.setB(4) encoded.c = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"c":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":5,"c":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.getB()).isEqualTo(-1) assertThat(decoded.c).isEqualTo(6) } @JsonClass(generateAdapter = true) class TransientProperty { @Transient var a: Int = -1 @Transient private var b: Int = -1 var c: Int = -1 fun getB() = b fun setB(b: Int) { this.b = b } } @Test fun transientDelegateProperty() { val jsonAdapter = moshi.adapter() val encoded = TransientDelegateProperty() encoded.a = 3 encoded.setB(4) encoded.c = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"c":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":5,"c":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.getB()).isEqualTo(-1) assertThat(decoded.c).isEqualTo(6) } @JsonClass(generateAdapter = true) class TransientDelegateProperty { private fun delegate(initial: T) = Delegates.observable(initial) { _, _, _ -> } @delegate:Transient var a: Int by delegate(-1) @delegate:Transient private var b: Int by delegate(-1) var c: Int by delegate(-1) @JvmName("getBPublic") fun getB() = b @JvmName("setBPublic") fun setB(b: Int) { this.b = b } } @Test fun manyProperties32() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = ManyProperties32( 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, ) val json = (""" |{ |"v01":101,"v02":102,"v03":103,"v04":104,"v05":105, |"v06":106,"v07":107,"v08":108,"v09":109,"v10":110, |"v11":111,"v12":112,"v13":113,"v14":114,"v15":115, |"v16":116,"v17":117,"v18":118,"v19":119,"v20":120, |"v21":121,"v22":122,"v23":123,"v24":124,"v25":125, |"v26":126,"v27":127,"v28":128,"v29":129,"v30":130, |"v31":131,"v32":132 |} |""") .trimMargin() .replace("\n", "") assertThat(jsonAdapter.toJson(encoded)).isEqualTo(json) val decoded = jsonAdapter.fromJson(json) assertThat(decoded.v01).isEqualTo(101) assertThat(decoded.v32).isEqualTo(132) } @JsonClass(generateAdapter = true) class ManyProperties32( var v01: Int, var v02: Int, var v03: Int, var v04: Int, var v05: Int, var v06: Int, var v07: Int, var v08: Int, var v09: Int, var v10: Int, var v11: Int, var v12: Int, var v13: Int, var v14: Int, var v15: Int, var v16: Int, var v17: Int, var v18: Int, var v19: Int, var v20: Int, var v21: Int, var v22: Int, var v23: Int, var v24: Int, var v25: Int, var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: Int, var v31: Int, var v32: Int, ) @Test fun manyProperties33() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = ManyProperties33( 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, ) val json = (""" |{ |"v01":101,"v02":102,"v03":103,"v04":104,"v05":105, |"v06":106,"v07":107,"v08":108,"v09":109,"v10":110, |"v11":111,"v12":112,"v13":113,"v14":114,"v15":115, |"v16":116,"v17":117,"v18":118,"v19":119,"v20":120, |"v21":121,"v22":122,"v23":123,"v24":124,"v25":125, |"v26":126,"v27":127,"v28":128,"v29":129,"v30":130, |"v31":131,"v32":132,"v33":133 |} |""") .trimMargin() .replace("\n", "") assertThat(jsonAdapter.toJson(encoded)).isEqualTo(json) val decoded = jsonAdapter.fromJson(json) assertThat(decoded.v01).isEqualTo(101) assertThat(decoded.v32).isEqualTo(132) assertThat(decoded.v33).isEqualTo(133) } @JsonClass(generateAdapter = true) class ManyProperties33( var v01: Int, var v02: Int, var v03: Int, var v04: Int, var v05: Int, var v06: Int, var v07: Int, var v08: Int, var v09: Int, var v10: Int, var v11: Int, var v12: Int, var v13: Int, var v14: Int, var v15: Int, var v16: Int, var v17: Int, var v18: Int, var v19: Int, var v20: Int, var v21: Int, var v22: Int, var v23: Int, var v24: Int, var v25: Int, var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: Int, var v31: Int, var v32: Int, var v33: Int, ) @Test fun unsettablePropertyIgnored() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = UnsettableProperty() encoded.b = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class UnsettableProperty { val a: Int = -1 var b: Int = -1 } @Test fun getterOnlyNoBackingField() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = GetterOnly(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) assertThat(decoded.total).isEqualTo(10) } @JsonClass(generateAdapter = true) class GetterOnly(var a: Int, var b: Int) { val total: Int get() = a + b } @Test fun getterAndSetterNoBackingField() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = GetterAndSetter(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5,"total":8}""") // Whether b is 6 or 7 is an implementation detail. Currently we call constructors then setters. val decoded1 = jsonAdapter.fromJson("""{"a":4,"b":6,"total":11}""") assertThat(decoded1.a).isEqualTo(4) assertThat(decoded1.b).isEqualTo(7) assertThat(decoded1.total).isEqualTo(11) // Whether b is 6 or 7 is an implementation detail. Currently we call constructors then setters. val decoded2 = jsonAdapter.fromJson("""{"a":4,"total":11,"b":6}""") assertThat(decoded2.a).isEqualTo(4) assertThat(decoded2.b).isEqualTo(7) assertThat(decoded2.total).isEqualTo(11) } @JsonClass(generateAdapter = true) class GetterAndSetter(var a: Int, var b: Int) { var total: Int get() = a + b set(value) { b = value - a } } @Test fun supertypeConstructorParameters() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = SubtypeConstructorParameters(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } open class SupertypeConstructorParameters(var a: Int) @JsonClass(generateAdapter = true) class SubtypeConstructorParameters(a: Int, var b: Int) : SupertypeConstructorParameters(a) @Test fun supertypeProperties() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = SubtypeProperties() encoded.a = 3 encoded.b = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5,"a":3}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } open class SupertypeProperties { var a: Int = -1 } @JsonClass(generateAdapter = true) class SubtypeProperties : SupertypeProperties() { var b: Int = -1 } /** Generated adapters don't track enough state to detect duplicated values. */ @Ignore @Test fun duplicatedValueParameter() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() try { jsonAdapter.fromJson("""{"a":4,"a":4}""") fail() } catch (expected: JsonDataException) { assertThat(expected).hasMessageThat().isEqualTo("Multiple values for 'a' at $.a") } } class DuplicateValueParameter(var a: Int = -1, var b: Int = -2) /** Generated adapters don't track enough state to detect duplicated values. */ @Ignore @Test fun duplicatedValueProperty() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() try { jsonAdapter.fromJson("""{"a":4,"a":4}""") fail() } catch (expected: JsonDataException) { assertThat(expected).hasMessageThat().isEqualTo("Multiple values for 'a' at $.a") } } class DuplicateValueProperty { var a: Int = -1 var b: Int = -2 } @Test fun extensionProperty() { val moshi = Moshi.Builder().build() val jsonAdapter = moshi.adapter() val encoded = ExtensionProperty(3) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) } @JsonClass(generateAdapter = true) class ExtensionProperty(var a: Int) var ExtensionProperty.b: Int get() { throw AssertionError() } set(value) { throw AssertionError() } /** https://github.com/square/moshi/issues/563 */ @Test fun qualifiedAdaptersAreShared() { val moshi = Moshi.Builder().add(UppercaseJsonAdapter()).build() val jsonAdapter = moshi.adapter() val encoded = MultiplePropertiesShareAdapter("Android", "Banana") assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"BANANA"}""") val delegateAdapters = GeneratedAdaptersTest_MultiplePropertiesShareAdapterJsonAdapter::class .memberProperties .filter { it.returnType.classifier == JsonAdapter::class } assertThat(delegateAdapters).hasSize(1) } @JsonClass(generateAdapter = true) class MultiplePropertiesShareAdapter( @Uppercase(true) var a: String, @Uppercase(true) var b: String, ) @Test fun toJsonOnly() { val moshi = Moshi.Builder().add(CustomToJsonOnlyAdapter()).build() val jsonAdapter = moshi.adapter() assertThat(jsonAdapter.toJson(CustomToJsonOnly(1, 2))).isEqualTo("""[1,2]""") val fromJson = jsonAdapter.fromJson("""{"a":3,"b":4}""") assertThat(fromJson.a).isEqualTo(3) assertThat(fromJson.b).isEqualTo(4) } @JsonClass(generateAdapter = true) class CustomToJsonOnly(var a: Int, var b: Int) class CustomToJsonOnlyAdapter { @ToJson fun toJson(v: CustomToJsonOnly): List { return listOf(v.a, v.b) } } @Test fun fromJsonOnly() { val moshi = Moshi.Builder().add(CustomFromJsonOnlyAdapter()).build() val jsonAdapter = moshi.adapter() assertThat(jsonAdapter.toJson(CustomFromJsonOnly(1, 2))).isEqualTo("""{"a":1,"b":2}""") val fromJson = jsonAdapter.fromJson("""[3,4]""") assertThat(fromJson.a).isEqualTo(3) assertThat(fromJson.b).isEqualTo(4) } @JsonClass(generateAdapter = true) class CustomFromJsonOnly(var a: Int, var b: Int) class CustomFromJsonOnlyAdapter { @FromJson fun fromJson(v: List): CustomFromJsonOnly { return CustomFromJsonOnly(v[0], v[1]) } } @Test fun privateTransientIsIgnored() { val jsonAdapter = moshi.adapter() val privateTransient = PrivateTransient() privateTransient.writeA(1) privateTransient.b = 2 assertThat(jsonAdapter.toJson(privateTransient)).isEqualTo("""{"b":2}""") val fromJson = jsonAdapter.fromJson("""{"a":3,"b":4}""") assertThat(fromJson.readA()).isEqualTo(-1) assertThat(fromJson.b).isEqualTo(4) } @JsonClass(generateAdapter = true) class PrivateTransient { @Transient private var a: Int = -1 var b: Int = -1 fun readA(): Int { return a } fun writeA(a: Int) { this.a = a } } @Test fun propertyIsNothing() { val moshi = Moshi.Builder().add(NothingAdapter()).build() val jsonAdapter = moshi.adapter().serializeNulls() val toJson = HasNothingProperty() toJson.a = "1" assertThat(jsonAdapter.toJson(toJson)).isEqualTo("""{"a":"1","b":null}""") val fromJson = jsonAdapter.fromJson("""{"a":"3","b":null}""") assertThat(fromJson.a).isEqualTo("3") assertNull(fromJson.b) } class NothingAdapter { @ToJson fun toJson(jsonWriter: JsonWriter, unused: Nothing?) { jsonWriter.nullValue() } @FromJson fun fromJson(jsonReader: JsonReader): Nothing? { jsonReader.skipValue() return null } } @JsonClass(generateAdapter = true) class HasNothingProperty { var a: String? = null var b: Nothing? = null } @Test fun enclosedParameterizedType() { val jsonAdapter = moshi.adapter() assertThat(jsonAdapter.toJson(HasParameterizedProperty(Twins("1", "2")))) .isEqualTo("""{"twins":{"a":"1","b":"2"}}""") val hasParameterizedProperty = jsonAdapter.fromJson("""{"twins":{"a":"3","b":"4"}}""") assertThat(hasParameterizedProperty.twins.a).isEqualTo("3") assertThat(hasParameterizedProperty.twins.b).isEqualTo("4") } @JsonClass(generateAdapter = true) class Twins(var a: T, var b: T) @JsonClass(generateAdapter = true) class HasParameterizedProperty(val twins: Twins) @Test fun uppercasePropertyName() { val adapter = moshi.adapter() val instance = adapter.fromJson("""{"AAA":1,"BBB":2}""") assertThat(instance.AAA).isEqualTo(1) assertThat(instance.BBB).isEqualTo(2) assertThat(adapter.toJson(UppercasePropertyName(3, 4))).isEqualTo("""{"AAA":3,"BBB":4}""") } @JsonClass(generateAdapter = true) class UppercasePropertyName(val AAA: Int, val BBB: Int) /** https://github.com/square/moshi/issues/574 */ @Test fun mutableUppercasePropertyName() { val adapter = moshi.adapter() val instance = adapter.fromJson("""{"AAA":1,"BBB":2}""") assertThat(instance.AAA).isEqualTo(1) assertThat(instance.BBB).isEqualTo(2) val value = MutableUppercasePropertyName() value.AAA = 3 value.BBB = 4 assertThat(adapter.toJson(value)).isEqualTo("""{"AAA":3,"BBB":4}""") } @JsonClass(generateAdapter = true) @Suppress("ktlint:standard:property-naming") class MutableUppercasePropertyName { var AAA: Int = -1 var BBB: Int = -1 } @JsonQualifier annotation class Uppercase( val inFrench: Boolean, val onSundays: Boolean = false, val temperature: Temperature = Temperature.COLD, ) { enum class Temperature { WARM, COLD, } } class UppercaseJsonAdapter { @ToJson fun toJson(@Uppercase(inFrench = true) s: String): String { return s.uppercase(Locale.US) } @FromJson @Uppercase(inFrench = true) fun fromJson(s: String): String { return s.lowercase(Locale.US) } } @JsonClass(generateAdapter = true) data class HasNullableBoolean(val boolean: Boolean?) @Test fun nullablePrimitivesUseBoxedPrimitiveAdapters() { val moshi = Moshi.Builder() .add( JsonAdapter.Factory { type, _, _ -> if (Boolean::class.javaObjectType == type) { return@Factory object : JsonAdapter() { override fun fromJson(reader: JsonReader): Boolean? { if (reader.peek() != JsonReader.Token.BOOLEAN) { reader.skipValue() return null } return reader.nextBoolean() } override fun toJson(writer: JsonWriter, value: Boolean?) { writer.value(value) } } } null } ) .build() val adapter = moshi.adapter().serializeNulls() assertThat(adapter.fromJson("""{"boolean":"not a boolean"}""")) .isEqualTo(HasNullableBoolean(null)) assertThat(adapter.toJson(HasNullableBoolean(null))).isEqualTo("""{"boolean":null}""") } @Test fun adaptersAreNullSafe() { val moshi = Moshi.Builder().build() val adapter = moshi.adapter() assertThat(adapter.fromJson("null")).isNull() assertThat(adapter.toJson(null)).isEqualTo("null") } @JsonClass(generateAdapter = true) data class HasCollectionOfPrimitives(val listOfInts: List) @Test fun hasCollectionOfPrimitives() { val moshi = Moshi.Builder().build() val adapter = moshi.adapter() val encoded = HasCollectionOfPrimitives(listOf(1, 2, -3)) assertThat(adapter.toJson(encoded)).isEqualTo("""{"listOfInts":[1,2,-3]}""") val decoded = adapter.fromJson("""{"listOfInts":[4,-5,6]}""") assertThat(decoded).isEqualTo(HasCollectionOfPrimitives(listOf(4, -5, 6))) } @JsonClass(generateAdapter = true, generator = "custom") data class CustomGeneratedClass(val foo: String) @Test fun customGenerator_withClassPresent() { val moshi = Moshi.Builder().build() val adapter = moshi.adapter() val unwrapped = (adapter as NullSafeJsonAdapter).delegate assertThat(unwrapped) .isInstanceOf(GeneratedAdaptersTest_CustomGeneratedClassJsonAdapter::class.java) } @JsonClass(generateAdapter = true, generator = "custom") data class CustomGeneratedClassMissing(val foo: String) @Test fun customGenerator_withClassMissing() { val moshi = Moshi.Builder().build() try { moshi.adapter() fail() } catch (e: RuntimeException) { assertThat(e).hasMessageThat().contains("Failed to find the generated JsonAdapter class") } } // https://github.com/square/moshi/issues/921 @Test fun internalPropertyWithoutBackingField() { val adapter = moshi.adapter() val test = InternalPropertyWithoutBackingField() assertThat(adapter.toJson(test)).isEqualTo("""{"bar":5}""") assertThat(adapter.fromJson("""{"bar":6}""").bar).isEqualTo(6) } @JsonClass(generateAdapter = true) class InternalPropertyWithoutBackingField { @Transient private var foo: Int = 5 internal var bar get() = foo set(f) { foo = f } } @JsonClass(generateAdapter = true) data class ClassWithFieldJson(@field:Json(name = "_links") val links: String) { @field:Json(name = "_ids") var ids: String? = null } // Regression test to ensure annotations with field site targets still use the right name @Test fun classWithFieldJsonTargets() { val moshi = Moshi.Builder().build() val adapter = moshi.adapter() // language=JSON val instance = adapter.fromJson("""{"_links": "link", "_ids": "id" }""") assertThat(instance).isEqualTo(ClassWithFieldJson("link").apply { ids = "id" }) } /* * These are a smoke test for https://github.com/square/moshi/issues/1023 to ensure that we * suppress deprecation warnings for using deprecated properties or classes. * * Ideally when stubs are fixed to actually included Deprecated annotations, we could then only * generate a deprecation suppression as needed and on targeted usages. * https://youtrack.jetbrains.com/issue/KT-34951 */ @Deprecated("Deprecated for reasons") @JsonClass(generateAdapter = true) data class DeprecatedClass(val foo: String) @JsonClass(generateAdapter = true) data class DeprecatedProperty(@Deprecated("Deprecated for reasons") val foo: String) @Test fun typesSizeCheckMessages_noArgs() { try { // Note: This is impossible to do if you use the reified adapter extension! moshi.adapter(MultipleGenerics::class.java) fail("Should have failed to construct the adapter due to missing generics") } catch (e: RuntimeException) { assertThat(e) .hasMessageThat() .isEqualTo( "Failed to find the generated JsonAdapter constructor for 'class com.squareup.moshi.kotlin.codegen.GeneratedAdaptersTest\$MultipleGenerics'. Suspiciously, the type was not parameterized but the target class 'com.squareup.moshi.kotlin.codegen.GeneratedAdaptersTest_MultipleGenericsJsonAdapter' is generic. Consider using Types#newParameterizedType() to define these missing type variables." ) } } @Test fun typesSizeCheckMessages_wrongNumberOfArgs() { try { GeneratedAdaptersTest_MultipleGenericsJsonAdapter( moshi, arrayOf(String::class.java), ) fail("Should have failed to construct the adapter due to wrong number of generics") } catch (e: IllegalArgumentException) { assertThat(e) .hasMessageThat() .isEqualTo( "TypeVariable mismatch: Expecting 4 types for generic type variables [A, B, C, D], but received 1" ) } } @JsonClass(generateAdapter = true) data class MultipleGenerics(val prop: String) @Test fun functionPropertyTypes() { val adapter = moshi.adapter() val json = "{\"id\":\"value\"}" assertThat(adapter.fromJson(json)).isEqualTo(LambdaTypeNames("value")) } // Regression test for https://github.com/square/moshi/issues/1265 @JsonClass(generateAdapter = true) data class LambdaTypeNames( val id: String, @Transient val simple: ((String) -> Boolean)? = null, // Receivers count as the first param, just annotated with a special annotation to indicate it's // a receiver @Transient val receiver: (String.(String) -> Boolean)? = null, // Tests that we use `FunctionN` since it has more than 23 params @Transient val arity: (String.( String, String, String, String, String, String, String, String, String, String, String, String, String, String, String, String, String, String, String, String, String, String, ) -> Boolean)? = null, ) } // Has to be outside to avoid Types seeing an owning class @JsonClass(generateAdapter = true) data class NullableTypeParams( val nullableList: List, val nullableSet: Set, val nullableMap: Map, val nullableT: T?, val nonNullT: T, ) ================================================ FILE: moshi-kotlin-tests/codegen-only/src/test/kotlin/com/squareup/moshi/kotlin/codegen/GeneratedAdaptersTest_CustomGeneratedClassJsonAdapter.kt ================================================ /* * Copyright (C) 2019 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 * * https://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.squareup.moshi.kotlin.codegen import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.kotlin.codegen.GeneratedAdaptersTest.CustomGeneratedClass // This also tests custom generated types with no moshi constructor @Suppress("ktlint:standard:class-naming") class GeneratedAdaptersTest_CustomGeneratedClassJsonAdapter : JsonAdapter() { override fun fromJson(reader: JsonReader): CustomGeneratedClass { TODO() } override fun toJson(writer: JsonWriter, value: CustomGeneratedClass?) { TODO() } } ================================================ FILE: moshi-kotlin-tests/codegen-only/src/test/kotlin/com/squareup/moshi/kotlin/codegen/LooksLikeAClass/ClassInPackageThatLooksLikeAClass.kt ================================================ /* * Copyright (C) 2020 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 * * https://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:Suppress("PackageName") package com.squareup.moshi.kotlin.codegen.LooksLikeAClass import com.squareup.moshi.JsonClass /** https://github.com/square/moshi/issues/783 */ @JsonClass(generateAdapter = true) data class ClassInPackageThatLooksLikeAClass(val foo: String) ================================================ FILE: moshi-kotlin-tests/codegen-only/src/test/kotlin/com/squareup/moshi/kotlin/codegen/MixingReflectAndCodeGen.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen import com.google.common.truth.Truth.assertThat import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import org.junit.Test class MixingReflectAndCodeGen { @Test fun mixingReflectionAndCodegen() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val generatedAdapter = moshi.adapter() val reflectionAdapter = moshi.adapter() assertThat(generatedAdapter.toString()) .isEqualTo("GeneratedJsonAdapter(MixingReflectAndCodeGen.UsesGeneratedAdapter).nullSafe()") assertThat(reflectionAdapter.toString()) .isEqualTo( "KotlinJsonAdapter(com.squareup.moshi.kotlin.codegen.MixingReflectAndCodeGen" + ".UsesReflectionAdapter).nullSafe()" ) } @JsonClass(generateAdapter = true) class UsesGeneratedAdapter(var a: Int, var b: Int) @JsonClass(generateAdapter = false) class UsesReflectionAdapter(var a: Int, var b: Int) } ================================================ FILE: moshi-kotlin-tests/codegen-only/src/test/kotlin/com/squareup/moshi/kotlin/codegen/MoshiKspTest.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen import com.google.common.truth.Truth.assertThat import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import org.junit.Test // Regression tests specific to Moshi-KSP class MoshiKspTest { private val moshi = Moshi.Builder().build() // Regression test for https://github.com/ZacSweers/MoshiX/issues/44 @Test fun onlyInterfaceSupertypes() { val adapter = moshi.adapter() // language=JSON val json = """{"a":"aValue","b":"bValue"}""" val expected = SimpleImpl("aValue", "bValue") val instance = adapter.fromJson(json) assertThat(instance).isEqualTo(expected) val encoded = adapter.toJson(instance) assertThat(encoded).isEqualTo(json) } interface SimpleInterface { val a: String } // NOTE the Any() superclass is important to test that we're detecting the farthest parent class // correct.y @JsonClass(generateAdapter = true) data class SimpleImpl(override val a: String, val b: String) : Any(), SimpleInterface } ================================================ FILE: moshi-kotlin-tests/codegen-only/src/test/kotlin/com/squareup/moshi/kotlin/codegen/MultipleMasksTest.kt ================================================ /* * Copyright (C) 2019 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 * * https://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.squareup.moshi.kotlin.codegen import com.squareup.moshi.JsonClass import com.squareup.moshi.Moshi import org.intellij.lang.annotations.Language import org.junit.Assert.assertEquals import org.junit.Test /** * This test explicitly tests mask generation for classes with more than 32 parameters. Each mask * can only indicate up to 32 parameters, so constructors with more than 32 parameters have to use * multiple masks. * * This covers a few cases of this: * - Ensuring values from json are matched to properties correctly * - Some `@Transient` parameters (which participate in the constructor signature and mask indices) * - This example has 3 total masks generated. * * Regression test for https://github.com/square/moshi/issues/977 */ class MultipleMasksTest { @Test fun testMultipleMasks() { // Set some arbitrary values to make sure offsets are aligning correctly @Language("JSON") val json = """{"arg50":500,"arg3":34,"arg11":11,"arg65":67}""" val instance = Moshi.Builder().build().adapter().fromJson(json) assertEquals(instance.arg2, 2) assertEquals(instance.arg3, 34) assertEquals(instance.arg11, 11) assertEquals(instance.arg49, 49) assertEquals(instance.arg50, 500) assertEquals(instance.arg65, 67) assertEquals(instance.arg64, 64) } } @JsonClass(generateAdapter = true) class MultipleMasks( val arg0: Long = 0, val arg1: Long = 1, val arg2: Long = 2, val arg3: Long = 3, val arg4: Long = 4, val arg5: Long = 5, val arg6: Long = 6, val arg7: Long = 7, val arg8: Long = 8, val arg9: Long = 9, val arg10: Long = 10, val arg11: Long, val arg12: Long = 12, val arg13: Long = 13, val arg14: Long = 14, val arg15: Long = 15, val arg16: Long = 16, val arg17: Long = 17, val arg18: Long = 18, val arg19: Long = 19, @Suppress("UNUSED_PARAMETER") arg20: Long = 20, val arg21: Long = 21, val arg22: Long = 22, val arg23: Long = 23, val arg24: Long = 24, val arg25: Long = 25, val arg26: Long = 26, val arg27: Long = 27, val arg28: Long = 28, val arg29: Long = 29, val arg30: Long = 30, val arg31: Long = 31, val arg32: Long = 32, val arg33: Long = 33, val arg34: Long = 34, val arg35: Long = 35, val arg36: Long = 36, val arg37: Long = 37, val arg38: Long = 38, @Transient val arg39: Long = 39, val arg40: Long = 40, val arg41: Long = 41, val arg42: Long = 42, val arg43: Long = 43, val arg44: Long = 44, val arg45: Long = 45, val arg46: Long = 46, val arg47: Long = 47, val arg48: Long = 48, val arg49: Long = 49, val arg50: Long = 50, val arg51: Long = 51, val arg52: Long = 52, @Transient val arg53: Long = 53, val arg54: Long = 54, val arg55: Long = 55, val arg56: Long = 56, val arg57: Long = 57, val arg58: Long = 58, val arg59: Long = 59, val arg60: Long = 60, val arg61: Long = 61, val arg62: Long = 62, val arg63: Long = 63, val arg64: Long = 64, val arg65: Long = 65, ) ================================================ FILE: moshi-kotlin-tests/codegen-only/src/test/kotlin/com/squareup/moshi/kotlin/codegen/annotation/UppercaseInAnnotationPackage.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.annotation import com.squareup.moshi.FromJson import com.squareup.moshi.JsonQualifier import com.squareup.moshi.ToJson import java.util.Locale @JsonQualifier annotation class UppercaseInAnnotationPackage class UppercaseInAnnotationPackageJsonAdapter { @ToJson fun toJson(@UppercaseInAnnotationPackage s: String): String { return s.uppercase(Locale.US) } @FromJson @UppercaseInAnnotationPackage fun fromJson(s: String): String { return s.lowercase(Locale.US) } } ================================================ FILE: moshi-kotlin-tests/extra-moshi-test-module/build.gradle.kts ================================================ plugins { kotlin("jvm") } dependencies { implementation(project(":moshi")) } ================================================ FILE: moshi-kotlin-tests/extra-moshi-test-module/src/main/kotlin/com/squareup/moshi/kotlin/codegen/test/extra/AbstractClassInModuleA.kt ================================================ /* * Copyright (C) 2021 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 * * https://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.squareup.moshi.kotlin.codegen.test.extra import com.squareup.moshi.Json public abstract class AbstractClassInModuleA { // Ignored/transient to ensure processor sees them across module boundaries. @Transient private lateinit var lateinitTransient: String @Transient private var regularTransient: String = "regularTransient" // Note that we target the field because otherwise it is stored on the synthetic holder method for // annotations, which isn't visible from kapt @field:Json(ignore = true) private lateinit var lateinitIgnored: String @field:Json(ignore = true) private var regularIgnored: String = "regularIgnored" } ================================================ FILE: moshi-kotlin-tests/src/test/kotlin/com/squareup/moshi/kotlin/DualKotlinTest.kt ================================================ /* * Copyright (C) 2020 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 * * https://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.squareup.moshi.kotlin import com.google.common.truth.Truth.assertThat import com.squareup.moshi.FromJson import com.squareup.moshi.Json import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonQualifier import com.squareup.moshi.Moshi import com.squareup.moshi.ToJson import com.squareup.moshi.Types import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlin.annotation.AnnotationRetention.RUNTIME import org.intellij.lang.annotations.Language import org.junit.Assert.fail import org.junit.Test class DualKotlinTest { @Suppress("UNCHECKED_CAST") private val moshi = Moshi.Builder() // If code gen ran, the generated adapter will be tried first. If it can't find it, it will // gracefully fall back to the KotlinJsonAdapter. This allows us to easily test both. .addLast(KotlinJsonAdapterFactory()) .build() @Test fun requiredValueAbsent() { val jsonAdapter = moshi.adapter() try { // language=JSON jsonAdapter.fromJson("""{"a":4}""") fail() } catch (expected: JsonDataException) { assertThat(expected).hasMessageThat().isEqualTo("Required value 'b' missing at $") } } @JsonClass(generateAdapter = true) class RequiredValueAbsent(var a: Int = 3, var b: Int) @Test fun requiredValueWithDifferentJsonNameAbsent() { val jsonAdapter = moshi.adapter() try { // language=JSON jsonAdapter.fromJson("""{"a":4}""") fail() } catch (expected: JsonDataException) { assertThat(expected) .hasMessageThat() .isEqualTo("Required value 'b' (JSON name 'bPrime') missing at \$") } } @JsonClass(generateAdapter = true) class RequiredValueWithDifferentJsonNameAbsent(var a: Int = 3, @Json(name = "bPrime") var b: Int) @Test fun nonNullPropertySetToNullFailsWithJsonDataException() { val jsonAdapter = moshi.adapter() try { // language=JSON jsonAdapter.fromJson("{\"a\":null}") fail() } catch (expected: JsonDataException) { assertThat(expected).hasMessageThat().isEqualTo("Non-null value 'a' was null at \$.a") } } @Test fun nonNullPropertySetToNullFromAdapterFailsWithJsonDataException() { val jsonAdapter = moshi .newBuilder() .add( object { @Suppress("UNUSED_PARAMETER") @FromJson fun fromJson(string: String): String? = null } ) .build() .adapter() try { // language=JSON jsonAdapter.fromJson("{\"a\":\"hello\"}") fail() } catch (expected: JsonDataException) { assertThat(expected).hasMessageThat().isEqualTo("Non-null value 'a' was null at \$.a") } } @JsonClass(generateAdapter = true) class HasNonNullProperty { var a: String = "" } @Test fun nonNullPropertyWithJsonNameSetToNullFailsWithJsonDataException() { val jsonAdapter = moshi.adapter() try { // language=JSON jsonAdapter.fromJson("{\"aPrime\":null}") fail() } catch (expected: JsonDataException) { assertThat(expected) .hasMessageThat() .isEqualTo("Non-null value 'a' (JSON name 'aPrime') was null at \$.aPrime") } } @Test fun nonNullPropertyWithJsonNameSetToNullFromAdapterFailsWithJsonDataException() { val jsonAdapter = moshi .newBuilder() .add( object { @Suppress("UNUSED_PARAMETER") @FromJson fun fromJson(string: String): String? = null } ) .build() .adapter() try { // language=JSON jsonAdapter.fromJson("{\"aPrime\":\"hello\"}") fail() } catch (expected: JsonDataException) { assertThat(expected) .hasMessageThat() .isEqualTo("Non-null value 'a' (JSON name 'aPrime') was null at \$.aPrime") } } @JsonClass(generateAdapter = true) class HasNonNullPropertyDifferentJsonName { @Json(name = "aPrime") var a: String = "" } @Test fun nonNullConstructorParameterCalledWithNullFailsWithJsonDataException() { val jsonAdapter = moshi.adapter() try { // language=JSON jsonAdapter.fromJson("{\"a\":null}") fail() } catch (expected: JsonDataException) { assertThat(expected).hasMessageThat().isEqualTo("Non-null value 'a' was null at \$.a") } } @Test fun nonNullConstructorParameterCalledWithNullFromAdapterFailsWithJsonDataException() { val jsonAdapter = moshi .newBuilder() .add( object { @Suppress("UNUSED_PARAMETER") @FromJson fun fromJson(string: String): String? = null } ) .build() .adapter() try { // language=JSON jsonAdapter.fromJson("{\"a\":\"hello\"}") fail() } catch (expected: JsonDataException) { assertThat(expected).hasMessageThat().isEqualTo("Non-null value 'a' was null at \$.a") } } @Retention(RUNTIME) annotation class Nullable @JsonClass(generateAdapter = true) data class HasNonNullConstructorParameter(val a: String) @JsonClass(generateAdapter = true) data class HasNullableConstructorParameter(val a: String?) @Test fun delegatesToInstalledAdaptersBeforeNullChecking() { val localMoshi = moshi .newBuilder() .add( object { @FromJson fun fromJson(@Nullable string: String?): String { return string ?: "fallback" } @ToJson fun toJson(@Nullable value: String?): String { return value ?: "fallback" } } ) .build() val hasNonNullConstructorParameterAdapter = localMoshi.adapter() assertThat( // language=JSON hasNonNullConstructorParameterAdapter.fromJson("{\"a\":null}") ) .isEqualTo(HasNonNullConstructorParameter("fallback")) val hasNullableConstructorParameterAdapter = localMoshi.adapter() assertThat( // language=JSON hasNullableConstructorParameterAdapter.fromJson("{\"a\":null}") ) .isEqualTo(HasNullableConstructorParameter("fallback")) // language=JSON assertThat(hasNullableConstructorParameterAdapter.toJson(HasNullableConstructorParameter(null))) .isEqualTo("{\"a\":\"fallback\"}") } @JsonClass(generateAdapter = true) data class HasNullableTypeWithGeneratedAdapter(val a: HasNonNullConstructorParameter?) @Test fun delegatesToInstalledAdaptersBeforeNullCheckingWithGeneratedAdapter() { val adapter = moshi.adapter() val encoded = HasNullableTypeWithGeneratedAdapter(null) // language=JSON assertThat(adapter.toJson(encoded)).isEqualTo("""{}""") // language=JSON assertThat(adapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null}""") // language=JSON val decoded = adapter.fromJson("""{"a":null}""") assertThat(decoded.a).isEqualTo(null) } @Test fun valueClass() { val adapter = moshi.adapter() val inline = ValueClass(5) val expectedJson = """{"i":5}""" assertThat(adapter.toJson(inline)).isEqualTo(expectedJson) val testJson = """{"i":6}""" val result = adapter.fromJson(testJson) assertThat(result.i).isEqualTo(6) val testEmptyJson = """{}""" val result2 = adapter.fromJson(testEmptyJson) assertThat(result2.i).isEqualTo(0) } @JsonClass(generateAdapter = false) data class InlineConsumer(val inline: ValueClass) @Test fun inlineClassConsumer() { val adapter = moshi.adapter() val consumer = InlineConsumer(ValueClass(23)) @Language("JSON") val expectedJson = """{"inline":{"i":23}}""" assertThat(adapter.toJson(consumer)).isEqualTo(expectedJson) @Language("JSON") val testJson = """{"inline":{"i":42}}""" val result = adapter.fromJson(testJson) assertThat(result.inline.i).isEqualTo(42) } // Regression test for https://github.com/square/moshi/issues/955 @Test fun backwardReferencingTypeVars() { val adapter = moshi.adapter() @Language("JSON") val testJson = """{"text":"text"}""" assertThat(adapter.toJson(TextAssetMetaData("text"))).isEqualTo(testJson) val result = adapter.fromJson(testJson) assertThat(result.text).isEqualTo("text") } @JsonClass(generateAdapter = true) class TextAssetMetaData(val text: String) : AssetMetaData() class TextAsset : Asset() abstract class Asset> abstract class AssetMetaData> // Regression test for https://github.com/ZacSweers/MoshiX/issues/125 @Test fun selfReferencingTypeVars() { val adapter = moshi.adapter() val data = StringNodeNumberNode().also { it.t = StringNodeNumberNode().also { it.text = "child 1" } it.text = "root" it.r = NumberStringNode().also { it.number = 0 it.t = NumberStringNode().also { it.number = 1 } it.r = StringNodeNumberNode().also { it.text = "grand child 1" } } } assertThat(adapter.toJson(data)) // language=JSON .isEqualTo( """ {"text":"root","r":{"number":0,"r":{"text":"grand child 1"},"t":{"number":1}},"t":{"text":"child 1"}} """ .trimIndent() ) } @JsonClass(generateAdapter = true) open class Node, R : Node> { // kotlin-reflect doesn't preserve ordering, so put these in alphabetical order so that // both reflective and code gen tests work the same var r: R? = null var t: T? = null } @JsonClass(generateAdapter = true) class StringNodeNumberNode : Node() { var text: String = "" } @JsonClass(generateAdapter = true) class NumberStringNode : Node() { var number: Int = 0 } // Regression test for https://github.com/square/moshi/issues/968 @Test fun abstractSuperProperties() { val adapter = moshi.adapter() @Language("JSON") val testJson = """{"test":"text"}""" assertThat(adapter.toJson(InternalAbstractProperty("text"))).isEqualTo(testJson) val result = adapter.fromJson(testJson) assertThat(result.test).isEqualTo("text") } abstract class InternalAbstractPropertyBase { internal abstract val test: String // Regression for https://github.com/square/moshi/issues/974 abstract fun abstractFun(): String } @JsonClass(generateAdapter = true) class InternalAbstractProperty(override val test: String) : InternalAbstractPropertyBase() { override fun abstractFun(): String { return test } } // Regression test for https://github.com/square/moshi/issues/975 @Test fun multipleConstructors() { val adapter = moshi.adapter() // language=JSON assertThat(adapter.toJson(MultipleConstructorsB(6))).isEqualTo("""{"f":{"f":6},"b":6}""") @Language("JSON") val testJson = """{"b":6}""" val result = adapter.fromJson(testJson) assertThat(result.b).isEqualTo(6) } @JsonClass(generateAdapter = true) class MultipleConstructorsA(val f: Int) @JsonClass(generateAdapter = true) class MultipleConstructorsB(val f: MultipleConstructorsA = MultipleConstructorsA(5), val b: Int) { constructor(f: Int, b: Int = 6) : this(MultipleConstructorsA(f), b) } @Test fun `multiple non-property parameters`() { val adapter = moshi.adapter() @Language("JSON") val testJson = """{"prop":7}""" assertThat(adapter.toJson(MultipleNonPropertyParameters(7))).isEqualTo(testJson) val result = adapter.fromJson(testJson) assertThat(result.prop).isEqualTo(7) } @JsonClass(generateAdapter = true) class MultipleNonPropertyParameters(val prop: Int, param1: Int = 1, param2: Int = 2) { init { // Ensure the params always uses their default value require(param1 == 1) require(param2 == 2) } } // Tests the case of multiple parameters with no parameter properties. @Test fun `only multiple non-property parameters`() { val adapter = moshi.adapter() @Language("JSON") val testJson = """{"prop":7}""" assertThat(adapter.toJson(OnlyMultipleNonPropertyParameters().apply { prop = 7 })) .isEqualTo(testJson) val result = adapter.fromJson(testJson) assertThat(result.prop).isEqualTo(7) } @JsonClass(generateAdapter = true) class OnlyMultipleNonPropertyParameters(param1: Int = 1, param2: Int = 2) { init { // Ensure the params always uses their default value require(param1 == 1) require(param2 == 2) } var prop: Int = 0 } @Test fun typeAliasUnwrapping() { val adapter = moshi .newBuilder() .add(Types.supertypeOf(Int::class.javaObjectType), moshi.adapter()) .build() .adapter() @Language("JSON") val testJson = """{"simpleClass":6,"parameterized":{"value":6},"wildcardIn":{"value":6},"wildcardOut":{"value":6},"complex":{"value":[{"value":6}]}}""" val testValue = TypeAliasUnwrapping( simpleClass = 6, parameterized = GenericClass(6), wildcardIn = GenericClass(6), wildcardOut = GenericClass(6), complex = GenericClass(listOf(GenericClass(6))), ) assertThat(adapter.toJson(testValue)).isEqualTo(testJson) val result = adapter.fromJson(testJson) assertThat(result).isEqualTo(testValue) } @JsonClass(generateAdapter = true) data class TypeAliasUnwrapping( val simpleClass: TypeAlias, val parameterized: GenericClass, val wildcardIn: GenericClass, val wildcardOut: GenericClass, @Suppress("REDUNDANT_NULLABLE") val complex: GenericClass?, ) // Regression test for https://github.com/square/moshi/issues/991 @Test fun nullablePrimitiveProperties() { val adapter = moshi.adapter() @Language("JSON") val testJson = """{"objectType":"value","boolean":true,"byte":3,"char":"a","short":3,"int":3,"long":3,"float":3.2,"double":3.2}""" val instance = NullablePrimitives( objectType = "value", boolean = true, byte = 3, char = 'a', short = 3, int = 3, long = 3, float = 3.2f, double = 3.2, ) assertThat(adapter.toJson(instance)).isEqualTo(testJson) val result = adapter.fromJson(testJson) assertThat(result).isEqualTo(instance) } @JsonClass(generateAdapter = true) data class NullablePrimitives( val objectType: String = "", val boolean: Boolean, val nullableBoolean: Boolean? = null, val byte: Byte, val nullableByte: Byte? = null, val char: Char, val nullableChar: Char? = null, val short: Short, val nullableShort: Short? = null, val int: Int, val nullableInt: Int? = null, val long: Long, val nullableLong: Long? = null, val float: Float, val nullableFloat: Float? = null, val double: Double, val nullableDouble: Double? = null, ) // Regression test for https://github.com/square/moshi/issues/990 @Test fun nullableProperties() { val adapter = moshi.adapter() @Language("JSON") val testJson = """{"nullableList":null}""" assertThat(adapter.serializeNulls().toJson(NullableList(null))).isEqualTo(testJson) val result = adapter.fromJson(testJson) assertThat(result.nullableList).isNull() } @JsonClass(generateAdapter = true) data class NullableList(val nullableList: List?) @Test fun typeAliasNullability() { val adapter = moshi.adapter() @Language("JSON") val testJson = """{"aShouldBeNonNull":3,"nullableAShouldBeNullable":null,"redundantNullableAShouldBeNullable":null,"manuallyNullableAShouldBeNullable":null,"convolutedMultiNullableShouldBeNullable":null,"deepNestedNullableShouldBeNullable":null}""" val instance = TypeAliasNullability(3, null, null, null, null, null) assertThat(adapter.serializeNulls().toJson(instance)).isEqualTo(testJson) val result = adapter.fromJson(testJson) assertThat(result).isEqualTo(instance) } @Suppress("REDUNDANT_NULLABLE") @JsonClass(generateAdapter = true) data class TypeAliasNullability( val aShouldBeNonNull: A, val nullableAShouldBeNullable: NullableA, val redundantNullableAShouldBeNullable: NullableA?, val manuallyNullableAShouldBeNullable: A?, val convolutedMultiNullableShouldBeNullable: NullableB?, val deepNestedNullableShouldBeNullable: E, ) // Regression test for https://github.com/square/moshi/issues/1009 @Test fun outDeclaration() { val adapter = moshi.adapter>() @Language("JSON") val testJson = """{"input":3}""" val instance = OutDeclaration(3) assertThat(adapter.serializeNulls().toJson(instance)).isEqualTo(testJson) val result = adapter.fromJson(testJson) assertThat(result).isEqualTo(instance) } @JsonClass(generateAdapter = true) data class OutDeclaration(val input: T) @Test fun intersectionTypes() { val adapter = moshi.adapter>() @Language("JSON") val testJson = """{"value":"VALUE"}""" val instance = IntersectionTypes(IntersectionTypesEnum.VALUE) assertThat(adapter.serializeNulls().toJson(instance)).isEqualTo(testJson) val result = adapter.fromJson(testJson) assertThat(result).isEqualTo(instance) } interface IntersectionTypeInterface> enum class IntersectionTypesEnum : IntersectionTypeInterface { VALUE } @JsonClass(generateAdapter = true) data class IntersectionTypes(val value: E) where E : Enum, E : IntersectionTypeInterface @Test fun transientConstructorParameter() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = TransientConstructorParameter(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.b).isEqualTo(6) } class TransientConstructorParameter(@Transient var a: Int = -1, var b: Int = -1) @Test fun multipleTransientConstructorParameters() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = MultipleTransientConstructorParameters(3, 5, 7) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.b).isEqualTo(6) assertThat(decoded.c).isEqualTo(-1) } class MultipleTransientConstructorParameters( @Transient var a: Int = -1, var b: Int = -1, @Transient var c: Int = -1, ) @Test fun transientProperty() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = TransientProperty() encoded.a = 3 encoded.setB(4) encoded.c = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"c":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":5,"c":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.getB()).isEqualTo(-1) assertThat(decoded.c).isEqualTo(6) } class TransientProperty { @Transient var a: Int = -1 @Transient private var b: Int = -1 var c: Int = -1 fun getB() = b fun setB(b: Int) { this.b = b } } @Test fun ignoredConstructorParameter() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = IgnoredConstructorParameter(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.b).isEqualTo(6) } class IgnoredConstructorParameter(@Json(ignore = true) var a: Int = -1, var b: Int = -1) @Test fun multipleIgnoredConstructorParameters() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = MultipleIgnoredConstructorParameters(3, 5, 7) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.b).isEqualTo(6) assertThat(decoded.c).isEqualTo(-1) } class MultipleIgnoredConstructorParameters( @Json(ignore = true) var a: Int = -1, var b: Int = -1, @Json(ignore = true) var c: Int = -1, ) @Test fun ignoredProperty() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = IgnoredProperty() encoded.a = 3 encoded.setB(4) encoded.c = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"c":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":5,"c":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.getB()).isEqualTo(-1) assertThat(decoded.c).isEqualTo(6) } class IgnoredProperty { @Json(ignore = true) var a: Int = -1 @Json(ignore = true) private var b: Int = -1 var c: Int = -1 fun getB() = b fun setB(b: Int) { this.b = b } } @Test fun propertyNameHasDollarSign() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val value = PropertyWithDollarSign("apple", "banana") val json = """{"${'$'}a":"apple","${'$'}b":"banana"}""" assertThat(jsonAdapter.toJson(value)).isEqualTo(json) assertThat(jsonAdapter.fromJson(json)).isEqualTo(value) } @JsonClass(generateAdapter = true) data class PropertyWithDollarSign(val `$a`: String, @Json(name = "\$b") val b: String) @Retention(RUNTIME) @JsonQualifier annotation class NestedEnum(val nested: Nested = Nested.A) { enum class Nested { A, B, C, } } @Test fun nestedEnumAnnotation() { val moshi = Moshi.Builder() .add( object { @FromJson @NestedEnum fun fromJson(string: String): String = string @ToJson fun toJson(@NestedEnum @Nullable value: String?): String { return value ?: "fallback" } } ) .add(KotlinJsonAdapterFactory()) .build() val jsonAdapter = moshi.adapter() val value = PropertyWithNestedEnumAnnotation("apple") val json = """{"value":"apple"}""" assertThat(jsonAdapter.toJson(value)).isEqualTo(json) assertThat(jsonAdapter.fromJson(json)).isEqualTo(value) } @JsonClass(generateAdapter = true) data class PropertyWithNestedEnumAnnotation(@NestedEnum val value: String) @Test fun inlineClass() { val adapter = moshi.adapter() // Test encoding - should output raw value, not wrapped in object val instance = InlineValueClass(42) assertThat(adapter.toJson(instance)).isEqualTo("42") // Test decoding from raw value val decoded = adapter.fromJson("123") assertThat(decoded.value).isEqualTo(123) } @Test fun inlineClassNullSafe() { val adapter = moshi.adapter() // The adapter is wrapped with nullSafe(), so null JSON returns a null object assertThat(adapter.fromJson("null")).isNull() assertThat(adapter.toJson(null)).isEqualTo("null") } @Test fun delegateClassWithMultiplePropertiesAreFine() { val adapter = moshi.adapter>() val location = adapter.fromJson("[{\"x\":3,\"y\":\"5\"}]") assertThat(location).isEqualTo(listOf(Location(Point(3, 5)))) } @JsonClass(generateAdapter = true, inline = true) data class Location(val point: Point) @JsonClass(generateAdapter = true) data class Point(val x: Int, val y: Int) } typealias TypeAlias = Int @Suppress("REDUNDANT_PROJECTION") typealias GenericTypeAlias = List?>? @JsonClass(generateAdapter = true) data class GenericClass(val value: T) // Has to be outside since value classes are only allowed on top level @JvmInline @JsonClass(generateAdapter = true) value class ValueClass(val i: Int = 0) // Inline value classes for testing @JsonClass(inline = true) @JvmInline @JsonClass(generateAdapter = true, inline = true) value class InlineValueClass(val value: Int) typealias A = Int typealias NullableA = A? typealias B = NullableA @Suppress("REDUNDANT_NULLABLE") typealias NullableB = B? typealias C = NullableA typealias D = C typealias E = D ================================================ FILE: moshi-kotlin-tests/src/test/kotlin/com/squareup/moshi/kotlin/reflect/KotlinJsonAdapterTest.kt ================================================ /* * 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 * * https://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.squareup.moshi.kotlin.reflect import com.google.common.truth.Truth.assertThat import com.squareup.moshi.FromJson import com.squareup.moshi.Json import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonQualifier import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.ToJson import java.lang.reflect.ParameterizedType import java.lang.reflect.WildcardType import java.util.Locale import java.util.SimpleTimeZone import kotlin.annotation.AnnotationRetention.RUNTIME import org.assertj.core.api.Assertions import org.junit.Assert.fail import org.junit.Test @Suppress("UNUSED", "UNUSED_PARAMETER") class KotlinJsonAdapterTest { @Test fun constructorParameters() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = ConstructorParameters(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } class ConstructorParameters(var a: Int, var b: Int) @Test fun properties() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = Properties() encoded.a = 3 encoded.b = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""") assertThat(decoded.a).isEqualTo(3) assertThat(decoded.b).isEqualTo(5) } class Properties { var a: Int = -1 var b: Int = -1 } @Test fun constructorParametersAndProperties() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = ConstructorParametersAndProperties(3) encoded.b = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } class ConstructorParametersAndProperties(var a: Int) { var b: Int = -1 } @Test fun immutableConstructorParameters() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = ImmutableConstructorParameters(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } class ImmutableConstructorParameters(val a: Int, val b: Int) @Test fun immutableProperties() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = ImmutableProperties(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":3,"b":5}""") assertThat(decoded.a).isEqualTo(3) assertThat(decoded.b).isEqualTo(5) } class ImmutableProperties(a: Int, b: Int) { val a = a val b = b } @Test fun constructorDefaults() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = ConstructorDefaultValues(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"b":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.b).isEqualTo(6) } class ConstructorDefaultValues(var a: Int = -1, var b: Int = -2) @Test fun duplicatedValueParameter() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() try { jsonAdapter.fromJson("""{"a":4,"a":4}""") fail() } catch (expected: JsonDataException) { assertThat(expected).hasMessageThat().isEqualTo("Multiple values for 'a' at $.a") } } class DuplicateValueParameter(var a: Int = -1, var b: Int = -2) @Test fun duplicatedValueProperty() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() try { jsonAdapter.fromJson("""{"a":4,"a":4}""") fail() } catch (expected: JsonDataException) { assertThat(expected).hasMessageThat().isEqualTo("Multiple values for 'a' at $.a") } } class DuplicateValueProperty { var a: Int = -1 var b: Int = -2 } @Test fun explicitNull() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = ExplicitNull(null, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":null,"b":6}""") assertThat(decoded.a).isEqualTo(null) assertThat(decoded.b).isEqualTo(6) } class ExplicitNull(var a: Int?, var b: Int?) @Test fun absentNull() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = AbsentNull(null, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") assertThat(jsonAdapter.serializeNulls().toJson(encoded)).isEqualTo("""{"a":null,"b":5}""") val decoded = jsonAdapter.fromJson("""{"b":6}""") assertThat(decoded.a).isNull() assertThat(decoded.b).isEqualTo(6) } class AbsentNull(var a: Int?, var b: Int?) @Test fun repeatedValue() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() try { jsonAdapter.fromJson("""{"a":4,"b":null,"b":6}""") fail() } catch (expected: JsonDataException) { assertThat(expected).hasMessageThat().isEqualTo("Multiple values for 'b' at $.b") } } class RepeatedValue(var a: Int, var b: Int?) @Test fun constructorParameterWithQualifier() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).add(UppercaseJsonAdapter()).build() val jsonAdapter = moshi.adapter() val encoded = ConstructorParameterWithQualifier("Android", "Banana") assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""") val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""") assertThat(decoded.a).isEqualTo("android") assertThat(decoded.b).isEqualTo("Banana") } class ConstructorParameterWithQualifier(@Uppercase var a: String, var b: String) @Test fun propertyWithQualifier() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).add(UppercaseJsonAdapter()).build() val jsonAdapter = moshi.adapter() val encoded = PropertyWithQualifier() encoded.a = "Android" encoded.b = "Banana" assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":"ANDROID","b":"Banana"}""") val decoded = jsonAdapter.fromJson("""{"a":"Android","b":"Banana"}""") assertThat(decoded.a).isEqualTo("android") assertThat(decoded.b).isEqualTo("Banana") } class PropertyWithQualifier { @Uppercase var a: String = "" var b: String = "" } @Test fun constructorParameterWithJsonName() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = ConstructorParameterWithJsonName(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } class ConstructorParameterWithJsonName(@Json(name = "key a") var a: Int, var b: Int) @Test fun propertyWithJsonName() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = PropertyWithJsonName() encoded.a = 3 encoded.b = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"key a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"key a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } class PropertyWithJsonName { @Json(name = "key a") var a: Int = -1 var b: Int = -1 } @Test fun requiredTransientConstructorParameterFails() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (expected: IllegalArgumentException) { assertThat(expected) .hasMessageThat() .isEqualTo( "No default value for transient/ignored constructor parameter 'a' on type " + "'com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest.RequiredTransientConstructorParameter'" ) } } class RequiredTransientConstructorParameter(@Transient var a: Int) @Test fun requiredIgnoredConstructorParameterFails() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (expected: IllegalArgumentException) { assertThat(expected) .hasMessageThat() .isEqualTo( "No default value for transient/ignored constructor parameter 'a' on type " + "'com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest.RequiredIgnoredConstructorParameter'" ) } } class RequiredIgnoredConstructorParameter(@Json(ignore = true) var a: Int) @Test fun constructorParametersAndPropertiesWithSameNamesMustHaveSameTypes() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (expected: IllegalArgumentException) { assertThat(expected) .hasMessageThat() .isEqualTo( "'a' has a constructor parameter of type " + "kotlin.Int but a property of type kotlin.String." ) } } class ConstructorParameterWithSameNameAsPropertyButDifferentType(a: Int) { var a = "boo" } @Test fun supertypeConstructorParameters() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = SubtypeConstructorParameters(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } open class SupertypeConstructorParameters(var a: Int) class SubtypeConstructorParameters(a: Int, var b: Int) : SupertypeConstructorParameters(a) @Test fun supertypeProperties() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = SubtypeProperties() encoded.a = 3 encoded.b = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5,"a":3}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) } open class SupertypeProperties { var a: Int = -1 } class SubtypeProperties : SupertypeProperties() { var b: Int = -1 } @Test fun extendsPlatformClassWithPrivateField() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = ExtendsPlatformClassWithPrivateField(3) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3}""") val decoded = jsonAdapter.fromJson("""{"a":4,"id":"B"}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.id).isEqualTo("C") } internal class ExtendsPlatformClassWithPrivateField(var a: Int) : SimpleTimeZone(0, "C") @Test fun platformTypeThrows() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter>() fail() } catch (e: IllegalArgumentException) { assertThat(e) .hasMessageThat() .isEqualTo( "Platform class kotlin.Triple in kotlin.Triple requires explicit JsonAdapter to be registered" ) } } @Test fun privateConstructorParameters() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = PrivateConstructorParameters(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a()).isEqualTo(4) assertThat(decoded.b()).isEqualTo(6) } class PrivateConstructorParameters(private var a: Int, private var b: Int) { fun a() = a fun b() = b } @Test fun privateConstructor() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = PrivateConstructor.newInstance(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a()).isEqualTo(4) assertThat(decoded.b()).isEqualTo(6) } class PrivateConstructor private constructor(var a: Int, var b: Int) { fun a() = a fun b() = b companion object { fun newInstance(a: Int, b: Int) = PrivateConstructor(a, b) } } @Test fun privateProperties() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = PrivateProperties() encoded.a(3) encoded.b(5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a()).isEqualTo(4) assertThat(decoded.b()).isEqualTo(6) } class PrivateProperties { private var a: Int = -1 private var b: Int = -1 fun a() = a fun a(a: Int) { this.a = a } fun b() = b fun b(b: Int) { this.b = b } } @Test fun unsettablePropertyIgnored() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = UnsettableProperty() encoded.b = 5 assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(-1) assertThat(decoded.b).isEqualTo(6) } class UnsettableProperty { val a: Int = -1 var b: Int = -1 } @Test fun getterOnlyNoBackingField() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = GetterOnly(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5}""") val decoded = jsonAdapter.fromJson("""{"a":4,"b":6}""") assertThat(decoded.a).isEqualTo(4) assertThat(decoded.b).isEqualTo(6) assertThat(decoded.total).isEqualTo(10) } class GetterOnly(var a: Int, var b: Int) { val total: Int get() = a + b } @Test fun getterAndSetterNoBackingField() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = GetterAndSetter(3, 5) assertThat(jsonAdapter.toJson(encoded)).isEqualTo("""{"a":3,"b":5,"total":8}""") // Whether b is 6 or 7 is an implementation detail. Currently we call constructors then setters. val decoded1 = jsonAdapter.fromJson("""{"a":4,"b":6,"total":11}""") assertThat(decoded1.a).isEqualTo(4) assertThat(decoded1.b).isEqualTo(7) assertThat(decoded1.total).isEqualTo(11) // Whether b is 6 or 7 is an implementation detail. Currently we call constructors then setters. val decoded2 = jsonAdapter.fromJson("""{"a":4,"total":11,"b":6}""") assertThat(decoded2.a).isEqualTo(4) assertThat(decoded2.b).isEqualTo(7) assertThat(decoded2.total).isEqualTo(11) } class GetterAndSetter(var a: Int, var b: Int) { var total: Int get() = a + b set(value) { b = value - a } } @Test fun nonPropertyConstructorParameter() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (expected: IllegalArgumentException) { assertThat(expected) .hasMessageThat() .isEqualTo( "No property for required constructor parameter 'a' on type " + "'${NonPropertyConstructorParameter::class.qualifiedName}'" ) } } class NonPropertyConstructorParameter(a: Int, val b: Int) @Test fun kotlinEnumsAreNotCovered() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val adapter = moshi.adapter() assertThat(adapter.fromJson("""{"e": "A"}""")).isEqualTo(UsingEnum(KotlinEnum.A)) } data class UsingEnum(val e: KotlinEnum) enum class KotlinEnum { A, B, } @Test fun interfacesNotSupported() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (e: IllegalArgumentException) { assertThat(e) .hasMessageThat() .isEqualTo( "No JsonAdapter for interface " + "com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest\$Interface (with no annotations)" ) } } interface Interface @Test fun abstractClassesNotSupported() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (e: IllegalArgumentException) { assertThat(e) .hasMessageThat() .isEqualTo( "Cannot serialize abstract class " + "com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest\$AbstractClass" ) } } abstract class AbstractClass(val a: Int) @Test fun innerClassesNotSupported() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (e: IllegalArgumentException) { assertThat(e) .hasMessageThat() .isEqualTo( "Cannot serialize inner class " + "com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest\$InnerClass" ) } } inner class InnerClass(val a: Int) @Test fun localClassesNotSupported() { class LocalClass(val a: Int) val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (e: IllegalArgumentException) { assertThat(e) .hasMessageThat() .isEqualTo( "Cannot serialize local class or object expression " + "com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest\$localClassesNotSupported\$LocalClass" ) } } @Test fun objectDeclarationsNotSupported() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (e: IllegalArgumentException) { assertThat(e) .hasMessageThat() .isEqualTo( "Cannot serialize object declaration " + "com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest\$ObjectDeclaration" ) } } object ObjectDeclaration { var a = 5 } @Test fun anonymousClassesNotSupported() { val expression = object : Any() { var a = 5 } val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter(expression.javaClass) fail() } catch (e: IllegalArgumentException) { // anonymous/local classes are slightly different in bytecode across JVM versions val javaVersion = System.getProperty("java.version") val type = if (javaVersion.startsWith("1.8")) { "local class or object expression" } else { "anonymous class" } assertThat(e) .hasMessageThat() .isEqualTo( "Cannot serialize $type " + "com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest\$anonymousClassesNotSupported" + "\$expression$1" ) } } @Test fun manyProperties32() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = ManyProperties32( 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, ) val json = (""" |{ |"v01":101,"v02":102,"v03":103,"v04":104,"v05":105, |"v06":106,"v07":107,"v08":108,"v09":109,"v10":110, |"v11":111,"v12":112,"v13":113,"v14":114,"v15":115, |"v16":116,"v17":117,"v18":118,"v19":119,"v20":120, |"v21":121,"v22":122,"v23":123,"v24":124,"v25":125, |"v26":126,"v27":127,"v28":128,"v29":129,"v30":130, |"v31":131,"v32":132 |} |""") .trimMargin() .replace("\n", "") assertThat(jsonAdapter.toJson(encoded)).isEqualTo(json) val decoded = jsonAdapter.fromJson(json) assertThat(decoded.v01).isEqualTo(101) assertThat(decoded.v32).isEqualTo(132) } class ManyProperties32( var v01: Int, var v02: Int, var v03: Int, var v04: Int, var v05: Int, var v06: Int, var v07: Int, var v08: Int, var v09: Int, var v10: Int, var v11: Int, var v12: Int, var v13: Int, var v14: Int, var v15: Int, var v16: Int, var v17: Int, var v18: Int, var v19: Int, var v20: Int, var v21: Int, var v22: Int, var v23: Int, var v24: Int, var v25: Int, var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: Int, var v31: Int, var v32: Int, ) @Test fun manyProperties33() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val jsonAdapter = moshi.adapter() val encoded = ManyProperties33( 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, ) val json = (""" |{ |"v01":101,"v02":102,"v03":103,"v04":104,"v05":105, |"v06":106,"v07":107,"v08":108,"v09":109,"v10":110, |"v11":111,"v12":112,"v13":113,"v14":114,"v15":115, |"v16":116,"v17":117,"v18":118,"v19":119,"v20":120, |"v21":121,"v22":122,"v23":123,"v24":124,"v25":125, |"v26":126,"v27":127,"v28":128,"v29":129,"v30":130, |"v31":131,"v32":132,"v33":133 |} |""") .trimMargin() .replace("\n", "") assertThat(jsonAdapter.toJson(encoded)).isEqualTo(json) val decoded = jsonAdapter.fromJson(json) assertThat(decoded.v01).isEqualTo(101) assertThat(decoded.v32).isEqualTo(132) assertThat(decoded.v33).isEqualTo(133) } class ManyProperties33( var v01: Int, var v02: Int, var v03: Int, var v04: Int, var v05: Int, var v06: Int, var v07: Int, var v08: Int, var v09: Int, var v10: Int, var v11: Int, var v12: Int, var v13: Int, var v14: Int, var v15: Int, var v16: Int, var v17: Int, var v18: Int, var v19: Int, var v20: Int, var v21: Int, var v22: Int, var v23: Int, var v24: Int, var v25: Int, var v26: Int, var v27: Int, var v28: Int, var v29: Int, var v30: Int, var v31: Int, var v32: Int, var v33: Int, ) data class Box(val data: T) @Test fun genericTypes() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val stringBoxAdapter = moshi.adapter>() assertThat(stringBoxAdapter.fromJson("""{"data":"hello"}""")).isEqualTo(Box("hello")) assertThat(stringBoxAdapter.toJson(Box("hello"))).isEqualTo("""{"data":"hello"}""") } data class NestedGenerics(val value: Map>>) @Test fun nestedGenericTypes() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val adapter = moshi.adapter>>().indent(" ") val json = """ |{ | "value": { | "hello": { | "1": [ | { | "data": " " | }, | { | "data": "world!" | } | ] | } | } |} """ .trimMargin() val value = NestedGenerics(mapOf("hello" to mapOf(1 to listOf(Box(" "), Box("world!"))))) assertThat(adapter.fromJson(json)).isEqualTo(value) assertThat(adapter.toJson(value)).isEqualTo(json) } @Retention(RUNTIME) @JsonQualifier annotation class Uppercase class UppercaseJsonAdapter { @ToJson fun toJson(@Uppercase s: String): String { return s.uppercase(Locale.US) } @FromJson @Uppercase fun fromJson(s: String): String { return s.lowercase(Locale.US) } } data class HasNullableBoolean(val boolean: Boolean?) @Test fun nullablePrimitivesUseBoxedPrimitiveAdapters() { val moshi = Moshi.Builder() .add( JsonAdapter.Factory { type, _, _ -> if (Boolean::class.javaObjectType == type) { return@Factory object : JsonAdapter() { override fun fromJson(reader: JsonReader): Boolean? { if (reader.peek() != JsonReader.Token.BOOLEAN) { reader.skipValue() return null } return reader.nextBoolean() } override fun toJson(writer: JsonWriter, value: Boolean?) { writer.value(value) } } } null } ) .add(KotlinJsonAdapterFactory()) .build() val adapter = moshi.adapter().serializeNulls() assertThat(adapter.fromJson("""{"boolean":"not a boolean"}""")) .isEqualTo(HasNullableBoolean(null)) assertThat(adapter.toJson(HasNullableBoolean(null))).isEqualTo("""{"boolean":null}""") } @Test fun adaptersAreNullSafe() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val adapter = moshi.adapter() assertThat(adapter.fromJson("null")).isNull() assertThat(adapter.toJson(null)).isEqualTo("null") } @Test fun kotlinClassesWithoutAdapterAreRefused() { val moshi = Moshi.Builder().build() try { moshi.adapter() fail("Should not pass here") } catch (e: IllegalArgumentException) { assertThat(e).hasMessageThat().contains("Reflective serialization of Kotlin classes") } } class PlainKotlinClass @Test fun mapOfStringToStandardReflectionWildcards() { mapWildcardsParameterizedTest( MapOfStringToStandardReflection::class.java, """{"map":{"key":"value"}}""", MapOfStringToStandardReflection(mapOf("key" to "value")), ) } @JvmSuppressWildcards(suppress = false) data class MapOfStringToStandardReflection(val map: Map = mapOf()) @Test fun mapOfStringToStandardCodegenWildcards() { mapWildcardsParameterizedTest( MapOfStringToStandardCodegen::class.java, """{"map":{"key":"value"}}""", MapOfStringToStandardCodegen(mapOf("key" to "value")), ) } @JsonClass(generateAdapter = true) @JvmSuppressWildcards(suppress = false) data class MapOfStringToStandardCodegen(val map: Map = mapOf()) @Test fun mapOfStringToEnumReflectionWildcards() { mapWildcardsParameterizedTest( MapOfStringToEnumReflection::class.java, """{"map":{"key":"A"}}""", MapOfStringToEnumReflection(mapOf("key" to KotlinEnum.A)), ) } @JvmSuppressWildcards(suppress = false) data class MapOfStringToEnumReflection(val map: Map = mapOf()) @Test fun mapOfStringToEnumCodegenWildcards() { mapWildcardsParameterizedTest( MapOfStringToEnumCodegen::class.java, """{"map":{"key":"A"}}""", MapOfStringToEnumCodegen(mapOf("key" to KotlinEnum.A)), ) } @JsonClass(generateAdapter = true) @JvmSuppressWildcards(suppress = false) data class MapOfStringToEnumCodegen(val map: Map = mapOf()) @Test fun mapOfStringToCollectionReflectionWildcards() { mapWildcardsParameterizedTest( MapOfStringToCollectionReflection::class.java, """{"map":{"key":[]}}""", MapOfStringToCollectionReflection(mapOf("key" to listOf())), ) } @JvmSuppressWildcards(suppress = false) data class MapOfStringToCollectionReflection(val map: Map> = mapOf()) @Test fun mapOfStringToCollectionCodegenWildcards() { mapWildcardsParameterizedTest( MapOfStringToCollectionCodegen::class.java, """{"map":{"key":[]}}""", MapOfStringToCollectionCodegen(mapOf("key" to listOf())), ) } @JsonClass(generateAdapter = true) @JvmSuppressWildcards(suppress = false) data class MapOfStringToCollectionCodegen(val map: Map> = mapOf()) @Test fun mapOfStringToMapReflectionWildcards() { mapWildcardsParameterizedTest( MapOfStringToMapReflection::class.java, """{"map":{"key":{}}}""", MapOfStringToMapReflection(mapOf("key" to mapOf())), ) } @JvmSuppressWildcards(suppress = false) data class MapOfStringToMapReflection(val map: Map> = mapOf()) @Test fun mapOfStringToMapCodegenWildcards() { mapWildcardsParameterizedTest( MapOfStringToMapCodegen::class.java, """{"map":{"key":{}}}""", MapOfStringToMapCodegen(mapOf("key" to mapOf())), ) } @JsonClass(generateAdapter = true) @JvmSuppressWildcards(suppress = false) data class MapOfStringToMapCodegen(val map: Map> = mapOf()) @Test fun mapOfStringToArrayReflectionWildcards() { mapWildcardsParameterizedTest( MapOfStringToArrayReflection::class.java, """{"map":{"key":[]}}""", MapOfStringToArrayReflection(mapOf("key" to arrayOf())), ) } @JvmSuppressWildcards(suppress = false) data class MapOfStringToArrayReflection(val map: Map> = mapOf()) @Test fun mapOfStringToArrayCodegenWildcards() { mapWildcardsParameterizedTest( MapOfStringToArrayCodegen::class.java, """{"map":{"key":[]}}""", MapOfStringToArrayCodegen(mapOf("key" to arrayOf())), ) } @JsonClass(generateAdapter = true) @JvmSuppressWildcards(suppress = false) data class MapOfStringToArrayCodegen(val map: Map> = mapOf()) @Test fun mapOfStringToClassReflectionWildcards() { mapWildcardsParameterizedTest( MapOfStringToClassReflection::class.java, """{"map":{"key":{"a":19,"b":42}}}""", MapOfStringToClassReflection(mapOf("key" to ConstructorParameters(19, 42))), ) } @JvmSuppressWildcards(suppress = false) data class MapOfStringToClassReflection(val map: Map = mapOf()) @Test fun mapOfStringToClassCodegenWildcards() { mapWildcardsParameterizedTest( MapOfStringToClassCodegen::class.java, """{"map":{"key":{"a":19,"b":42}}}""", MapOfStringToClassCodegen(mapOf("key" to ConstructorParameters(19, 42))), ) } @JsonClass(generateAdapter = true) @JvmSuppressWildcards(suppress = false) data class MapOfStringToClassCodegen(val map: Map = mapOf()) @Test fun sealedClassesAreRejected() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (e: IllegalArgumentException) { assertThat(e).hasMessageThat().contains("Cannot reflectively serialize sealed class") } } sealed class SealedClass @Test fun inlineClassWithMultiplePropertiesFails() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (e: IllegalArgumentException) { assertThat(e) .hasMessageThat() .isEqualTo( "@JsonClass with inline = true requires exactly one non-transient property, " + "but " + "com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest.InlineWithMultipleProperties " + "has 2: a, b." ) } } @JsonClass(generateAdapter = false, inline = true) class InlineWithMultipleProperties(val a: Int, val b: Int) @Test fun inlineClassWithNullablePropertyFails() { val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() try { moshi.adapter() fail() } catch (e: IllegalArgumentException) { assertThat(e) .hasMessageThat() .isEqualTo( "@JsonClass with inline = true requires a non-nullable property, " + "but com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest.InlineWithNullableProperty.a is nullable." ) } } @JsonClass(generateAdapter = false, inline = true) class InlineWithNullableProperty(val a: Int?) private fun mapWildcardsParameterizedTest(type: Class, json: String, value: T) { // Ensure the map was created with the expected wildcards of a Kotlin map. val fieldType = type.getDeclaredField("map").genericType val fieldTypeArguments = (fieldType as ParameterizedType).actualTypeArguments assertThat(fieldTypeArguments[0]).isNotInstanceOf(WildcardType::class.java) assertThat(fieldTypeArguments[1]).isInstanceOf(WildcardType::class.java) val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() val adapter = moshi.adapter(type) @Suppress("DEPRECATION") Assertions.assertThat(adapter.fromJson(json)).isEqualToComparingFieldByFieldRecursively(value) assertThat(adapter.toJson(value)).isEqualTo(json) } } ================================================ FILE: releasing.md ================================================ # Releasing Cutting a Release ----------------- 1. Update the `CHANGELOG.md`: 1. Change the `Unreleased` header to the release version. 2. Add a link URL to ensure the header link works. 3. Add a new `Unreleased` section to the top. 2. Set versions: ``` export RELEASE_VERSION=X.Y.Z export NEXT_VERSION=X.Y.Z-SNAPSHOT ``` 3. Update versions, tag the release, and prepare for the next release. ```bash sed -i "" \ "s/VERSION_NAME=.*/VERSION_NAME=$RELEASE_VERSION/g" \ gradle.properties sed -i "" \ "s/\"com.squareup.moshi:\([^\:]*\):[0-9.]*\"/\"com.squareup.moshi:\1:$RELEASE_VERSION\"/g" \ `find . -name "README.md"` git commit -am "Prepare version $RELEASE_VERSION." git tag -am "Version $RELEASE_VERSION" $RELEASE_VERSION sed -i "" \ "s/VERSION_NAME=.*/VERSION_NAME=$NEXT_VERSION/g" \ gradle.properties git commit -am "Prepare next development version." git push && git push --tags ``` This will trigger a GitHub Action workflow which will create a GitHub release and upload the release artifacts to Maven Central. ================================================ FILE: settings.gradle.kts ================================================ pluginManagement { repositories { mavenCentral() gradlePluginPortal() } } rootProject.name = "moshi-root" include(":moshi") include(":moshi:japicmp") include(":moshi:records-tests") include(":moshi-adapters") include(":moshi-adapters:japicmp") include(":examples") include(":moshi-kotlin") include(":moshi-kotlin-codegen") include(":moshi-kotlin-tests") include(":moshi-kotlin-tests:codegen-only") include(":moshi-kotlin-tests:extra-moshi-test-module")