New Upstream Release - joda-convert

Ready changes

Summary

Merged new upstream version: 2.2.3 (was: 2.2.2).

Diff

diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..26aa42f
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,10 @@
+# Dependabot config
+
+version: 2
+updates:
+- package-ecosystem: "maven"
+  directory: "/"
+  schedule:
+    interval: weekly
+    time: "02:30"
+  open-pull-requests-limit: 20
diff --git a/.github/website.sh b/.github/website.sh
index c422e7c..b16fd0d 100644
--- a/.github/website.sh
+++ b/.github/website.sh
@@ -16,7 +16,7 @@ cp -R ../site joda-convert/
 echo "## update..."
 git add -A
 git status
-git commit --message "Update joda-beans from CI: $GITHUB_ACTION"
+git commit --message "Update joda-convert from CI: $GITHUB_ACTION"
 
 echo "## push..."
 git push origin main
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index fec3f4c..bb5e95a 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -10,40 +10,49 @@ on:
   pull_request:
     branches:
       - 'main'
+  schedule:
+    - cron: '41 19 * * 2'
+
+permissions:
+  contents: read
 
 jobs:
   build:
+    permissions:
+      security-events: write  # for github/codeql-action
     runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        java: [1.8, 11]
-    
     steps:
-    - name: Set up JDK
-      uses: actions/setup-java@v1
-      with:
-        java-version: ${{ matrix.java }}
     - name: Checkout
-      uses: actions/checkout@v2
-    - name: Maven cache
-      uses: actions/cache@v1
+      uses: actions/checkout@v3
+
+    - name: Set up JDK
+      uses: actions/setup-java@v3
       with:
-        path: ~/.m2/repository
-        key: ${{ runner.os }}-maven-${{ matrix.java }}-${{ hashFiles('**/pom.xml') }}-${{ hashFiles('.github/workflows/build.yml') }}
-        restore-keys: |
-          ${{ runner.os }}-maven-
+        java-version: 11
+        distribution: 'temurin'
+        cache: 'maven'
+
     - name: Maven version
       run: |
         mkdir -p ./.mvn
         echo '-e -B -DtrimStackTrace=false' > ./.mvn/maven.config
         mvn --version
         mkdir -p target
+
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
+      with:
+        languages: java
+    
     - name: Maven build
       run: |
         mvn install site
 
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v2
+    
     - name: Website
-      if: matrix.java == '11' && github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/website') || startsWith(github.ref, 'refs/tags/v'))
+      if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags/website') || startsWith(github.ref, 'refs/tags/v'))
       env:
         GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN_GH }}
       run: |
diff --git a/README.md b/README.md
index feec965..5a3c3e4 100644
--- a/README.md
+++ b/README.md
@@ -55,7 +55,7 @@ Various documentation is available:
 
 
 ### Releases
-[Release 2.2.2](https://www.joda.org/joda-convert/download.html) is the current latest release.
+[Release 2.2.3](https://www.joda.org/joda-convert/download.html) is the current latest release.
 This release is considered stable and worthy of the 2.x tag.
 The v2.x releases are compatible with v1.x releases, with the exception that the direct Guava dependency is removed.
 It depends on Java SE 6 or later.
diff --git a/debian/changelog b/debian/changelog
index a3a0081..1a96f92 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+joda-convert (2.2.3-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Mon, 29 Jan 2024 13:17:42 -0000
+
 joda-convert (2.2.2-1) unstable; urgency=medium
 
   * New upstream release
diff --git a/pom.xml b/pom.xml
index 6140248..ac9d244 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,32 +2,53 @@
 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
 
   <!-- ==================================================================== -->
-  <!-- Build requires Java SE 8 or later -->
-  <!-- Releases require Java SE 11 or later -->
+  <!-- Build requires Java SE 11 or later -->
   <!-- ==================================================================== -->
-  <parent>
-    <groupId>org.joda</groupId>
-    <artifactId>joda-parent</artifactId>
-    <version>1.4.0</version>
-  </parent>
   <modelVersion>4.0.0</modelVersion>
+  <groupId>org.joda</groupId>
   <artifactId>joda-convert</artifactId>
   <packaging>jar</packaging>
   <name>Joda-Convert</name>
-  <version>2.2.2</version>
+  <version>2.2.3</version>
   <description>Library to convert Objects to and from String</description>
-  <url>https://www.joda.org/${joda.artifactId}/</url>
+  <url>https://www.joda.org/joda-convert/</url>
 
   <!-- ==================================================================== -->
   <inceptionYear>2010</inceptionYear>
+  <licenses>
+    <license>
+      <name>Apache License, Version 2.0</name>
+      <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
+      <distribution>repo</distribution>
+    </license>
+  </licenses>
+  <organization>
+    <name>Joda.org</name>
+    <url>https://www.joda.org</url>
+  </organization>
+  <issueManagement>
+    <system>GitHub</system>
+    <url>https://github.com/JodaOrg/joda-convert/issues</url>
+  </issueManagement>
   <scm>
-    <connection>scm:git:https://github.com/JodaOrg/${joda.artifactId}.git</connection>
-    <developerConnection>scm:git:https://github.com/JodaOrg/${joda.artifactId}.git</developerConnection>
-    <url>https://github.com/JodaOrg/${joda.artifactId}</url>
-    <tag>v2.2.2</tag>
+    <connection>scm:git:https://github.com/JodaOrg/joda-convert.git</connection>
+    <developerConnection>scm:git:https://github.com/JodaOrg/joda-convert.git</developerConnection>
+    <url>https://github.com/JodaOrg/joda-convert</url>
+    <tag>v2.2.3</tag>
   </scm>
 
   <!-- ==================================================================== -->
+  <developers>
+    <developer>
+      <id>jodastephen</id>
+      <name>Stephen Colebourne</name>
+      <roles>
+        <role>Project Lead</role>
+      </roles>
+      <timezone>0</timezone>
+      <url>https://github.com/jodastephen</url>
+    </developer>
+  </developers>
   <contributors>
     <contributor>
       <name>Chris Kent</name>
@@ -41,21 +62,101 @@
 
   <!-- ==================================================================== -->
   <build>
+    <resources>
+      <resource>
+        <directory>src/main/resources</directory>
+      </resource>
+      <resource>
+        <targetPath>META-INF</targetPath>
+        <directory>${project.basedir}</directory>
+        <includes>
+          <include>LICENSE.txt</include>
+          <include>NOTICE.txt</include>
+        </includes>
+      </resource>
+    </resources>
+    <!-- define build -->
     <plugins>
-      <!-- Turn on Checkstyle -->
+      <!-- Enforce Maven 3.6.0 and Java 11+ -->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-checkstyle-plugin</artifactId>
+        <artifactId>maven-enforcer-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>enforce-maven</id>
+            <goals>
+              <goal>enforce</goal>
+            </goals>
+            <configuration>
+              <rules>
+                <requireMavenVersion>
+                  <version>3.6.0</version>
+                </requireMavenVersion>
+                <requireJavaVersion>
+                  <version>[11,)</version>
+                </requireJavaVersion>
+              </rules>
+            </configuration>
+          </execution>
+        </executions>
       </plugin>
-      <!-- Turn on JaCoCo -->
+      <!-- Compile twice -->
       <plugin>
-        <groupId>org.jacoco</groupId>
-        <artifactId>jacoco-maven-plugin</artifactId>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <executions>
+          <!-- compile first with module-info for Java 9 -->
+          <execution>
+            <id>default-compile</id>
+            <configuration>
+              <release>9</release>
+            </configuration>
+          </execution>
+          <!-- then compile without module-info for Java 8 -->
+          <execution>
+            <id>base-compile</id>
+            <goals>
+              <goal>compile</goal>
+            </goals>
+            <configuration>
+              <excludes>
+                <exclude>module-info.java</exclude>
+              </excludes>
+            </configuration>
+          </execution>
+        </executions>
+        <!-- setup defaults for compile and testCompile -->
+        <configuration>
+          <release>8</release>
+        </configuration>
+      </plugin>
+      <!-- Hack to extract dependencies for Surefire plugin -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>copy-dependencies</id>
+            <phase>compile</phase>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>${project.build.directory}/dependencies</outputDirectory>
+              <overWriteReleases>true</overWriteReleases>
+              <overWriteIfNewer>true</overWriteIfNewer>
+            </configuration>
+          </execution>
+        </executions>
       </plugin>
+      <!-- Surefire plugin is broken, https://issues.apache.org/jira/browse/SUREFIRE-1501 -->
       <!-- Setup surefire to test with Guava not present -->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-surefire-plugin</artifactId>
+        <configuration>
+          <argLine>--add-modules org.joda.convert --module-path ${project.build.directory}/dependencies ${argLine}</argLine>
+        </configuration>
         <executions>
           <!-- execute all suitable tests with guava not present -->
           <execution>
@@ -83,7 +184,334 @@
           </execution>
         </executions>
       </plugin>
+      <!-- Setup OSGi bundle data -->
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <version>${maven-bundle-plugin.version}</version>
+        <executions>
+          <execution>
+            <id>bundle-manifest</id>
+            <phase>process-classes</phase>
+            <goals>
+              <goal>manifest</goal>
+            </goals>
+            <configuration>
+              <instructions>
+                <Specification-Version>${project.version}</Specification-Version>
+                <Export-Package>${joda.osgi.packages}</Export-Package>
+                <Require-Capability>${joda.osgi.require.capability}</Require-Capability>
+              </instructions>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <!-- Setup Jar file manifest entries -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <configuration>
+          <archive>
+            <manifestFile>${project.build.outputDirectory}/META-INF/MANIFEST.MF</manifestFile>
+            <manifest>
+              <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
+              <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
+            </manifest>
+          </archive>
+        </configuration>
+      </plugin>
+      <!-- Setup Javadoc jar -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>attach-javadocs</id>
+            <phase>package</phase>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+        <!-- Javadoc uses source 11 to pickup the module settings -->
+        <configuration>
+          <source>11</source>
+        </configuration>
+      </plugin>
+      <!-- Setup source jar -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>attach-sources</id>
+            <phase>package</phase>
+            <goals>
+              <goal>jar-no-fork</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <!-- Setup Checkstyle, excluding module-info -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>run-checkstyle</id>
+            <phase>process-sources</phase>
+            <goals>
+              <goal>checkstyle</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <excludes>module-info.java</excludes>
+        </configuration>
+        <dependencies>
+          <dependency>
+            <groupId>com.puppycrawl.tools</groupId>
+            <artifactId>checkstyle</artifactId>
+            <version>${checkstyle.version}</version>
+          </dependency>
+        </dependencies>
+      </plugin>
+      <!-- Setup JaCoCo code coverage -->
+      <plugin>
+        <groupId>org.jacoco</groupId>
+        <artifactId>jacoco-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>jacoco-initialize</id>
+            <goals>
+              <goal>prepare-agent</goal>
+            </goals>
+          </execution>
+          <execution>
+            <id>jacoco-site</id>
+            <phase>package</phase>
+            <goals>
+              <goal>report</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+      <!-- Release to GitHub -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-release-plugin</artifactId>
+        <configuration>
+          <arguments>-Doss.repo</arguments>
+          <autoVersionSubmodules>true</autoVersionSubmodules>
+          <tagNameFormat>v@{project.version}</tagNameFormat>
+          <localCheckout>true</localCheckout>
+        </configuration>
+        <dependencies>
+          <dependency>
+            <groupId>org.kohsuke</groupId>
+            <artifactId>github-api</artifactId>
+            <version>${github-api.version}</version>
+          </dependency>
+        </dependencies>
+      </plugin>
     </plugins>
+    <!-- Manage plugin versions -->
+    <pluginManagement>
+      <plugins>
+        <!-- Maven build and reporting plugins (alphabetical) -->
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-assembly-plugin</artifactId>
+          <version>${maven-assembly-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-checkstyle-plugin</artifactId>
+          <version>${maven-checkstyle-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-changes-plugin</artifactId>
+          <version>${maven-changes-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-clean-plugin</artifactId>
+          <version>${maven-clean-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-compiler-plugin</artifactId>
+          <version>${maven-compiler-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-deploy-plugin</artifactId>
+          <version>${maven-deploy-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-dependency-plugin</artifactId>
+          <version>${maven-dependency-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-enforcer-plugin</artifactId>
+          <version>${maven-enforcer-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-gpg-plugin</artifactId>
+          <version>${maven-gpg-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-install-plugin</artifactId>
+          <version>${maven-install-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jar-plugin</artifactId>
+          <version>${maven-jar-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-javadoc-plugin</artifactId>
+          <version>${maven-javadoc-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-jxr-plugin</artifactId>
+          <version>${maven-jxr-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-plugin-plugin</artifactId>
+          <version>${maven-plugin-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-pmd-plugin</artifactId>
+          <version>${maven-pmd-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-project-info-reports-plugin</artifactId>
+          <version>${maven-project-info-reports-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-release-plugin</artifactId>
+          <version>${maven-release-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-repository-plugin</artifactId>
+          <version>${maven-repository-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-resources-plugin</artifactId>
+          <version>${maven-resources-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-source-plugin</artifactId>
+          <version>${maven-source-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>${maven-surefire-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-report-plugin</artifactId>
+          <version>${maven-surefire-report-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-toolchains-plugin</artifactId>
+          <version>${maven-toolchains-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>org.jacoco</groupId>
+          <artifactId>jacoco-maven-plugin</artifactId>
+          <version>${jacoco-maven-plugin.version}</version>
+        </plugin>
+        <plugin>
+          <groupId>com.github.spotbugs</groupId>
+          <artifactId>spotbugs-maven-plugin</artifactId>
+          <version>${spotbugs-maven-plugin.version}</version>
+        </plugin>
+        <!-- Setup site with reflow maven skin -->
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-site-plugin</artifactId>
+          <version>${maven-site-plugin.version}</version>
+          <configuration>
+            <skipDeploy>true</skipDeploy>
+          </configuration>
+          <executions>
+            <execution>
+              <id>attach-descriptor</id>
+              <goals>
+                <goal>attach-descriptor</goal>
+              </goals>
+              <!-- https://issues.apache.org/jira/browse/MSITE-639 -->
+              <configuration>
+                <locales>en,de</locales>
+              </configuration>
+            </execution>
+          </executions>
+          <dependencies>
+            <dependency>
+              <groupId>org.joda.external</groupId>
+              <artifactId>reflow-velocity-tools</artifactId>
+              <version>1.2</version>
+            </dependency>
+          </dependencies>
+        </plugin>
+        <!-- for Eclipse -->
+        <plugin>
+          <groupId>org.eclipse.m2e</groupId>
+          <artifactId>lifecycle-mapping</artifactId>
+          <version>1.0.0</version>
+          <configuration>
+            <lifecycleMappingMetadata>
+              <pluginExecutions>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>org.apache.felix</groupId>
+                    <artifactId>maven-bundle-plugin</artifactId>
+                    <versionRange>[2.5.4,)</versionRange>
+                    <goals>
+                      <goal>manifest</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore />
+                  </action>
+                </pluginExecution>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-dependency-plugin</artifactId>
+                    <versionRange>[3.1.1,)</versionRange>
+                    <goals>
+                      <goal>copy-dependencies</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore />
+                  </action>
+                </pluginExecution>
+              </pluginExecutions>
+            </lifecycleMappingMetadata>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
   </build>
 
   <!-- ==================================================================== -->
@@ -92,7 +520,7 @@
     <dependency>
       <groupId>com.google.guava</groupId>
       <artifactId>guava</artifactId>
-      <version>31.0.1-jre</version>
+      <version>31.1-jre</version>
       <scope>test</scope>
     </dependency>
     <dependency>
@@ -103,6 +531,132 @@
     </dependency>
   </dependencies>
 
+  <!-- ==================================================================== -->
+  <reporting>
+    <plugins>
+      <!-- Setup standard project info reports -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-project-info-reports-plugin</artifactId>
+        <version>${maven-project-info-reports-plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>ci-management</report>
+              <report>dependencies</report>
+              <report>dependency-info</report>
+              <report>issue-management</report>
+              <report>licenses</report>
+              <report>team</report>
+              <report>scm</report>
+              <report>summary</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+      <!-- Setup Checkstyle report, excluding module-info -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-checkstyle-plugin</artifactId>
+        <version>${maven-checkstyle-plugin.version}</version>
+        <configuration>
+          <includeResources>false</includeResources>
+          <includeTestResources>false</includeTestResources>
+          <includeTestSourceDirectory>false</includeTestSourceDirectory>
+          <excludes>module-info.java</excludes>
+        </configuration>
+      </plugin>
+      <!-- Setup Javadoc report -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-javadoc-plugin</artifactId>
+        <version>${maven-javadoc-plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>javadoc</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+        <configuration>
+          <source>11</source>
+        </configuration>
+      </plugin>
+      <!-- Setup Surefire report -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-report-plugin</artifactId>
+        <version>${maven-surefire-report-plugin.version}</version>
+        <configuration>
+          <showSuccess>true</showSuccess>
+        </configuration>
+      </plugin>
+      <!-- Setup changes (release notes) -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-changes-plugin</artifactId>
+        <version>${maven-changes-plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>changes-report</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+      <!-- Setup PMD report, excluding module-info -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-pmd-plugin</artifactId>
+        <version>${maven-pmd-plugin.version}</version>
+        <configuration>
+          <minimumTokens>100</minimumTokens>
+          <targetJdk>${maven.compiler.target}</targetJdk>
+          <excludes>
+            <exclude>module-info.java</exclude>
+          </excludes>
+        </configuration>
+      </plugin>
+      <!-- Setup spotbugs report -->
+      <plugin>
+        <groupId>com.github.spotbugs</groupId>
+        <artifactId>spotbugs-maven-plugin</artifactId>
+        <version>${spotbugs-maven-plugin.version}</version>
+      </plugin>
+      <!-- Setup JaCoCo report -->
+      <plugin>
+        <groupId>org.jacoco</groupId>
+        <artifactId>jacoco-maven-plugin</artifactId>
+        <version>${jacoco-maven-plugin.version}</version>
+        <reportSets>
+          <reportSet>
+            <reports>
+              <report>report</report>
+            </reports>
+          </reportSet>
+        </reportSets>
+      </plugin>
+    </plugins>
+  </reporting>
+
+  <!-- ==================================================================== -->
+  <distributionManagement>
+    <repository>
+      <id>sonatype-joda-staging</id>
+      <name>Sonatype OSS staging repository</name>
+      <url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
+      <layout>default</layout>
+    </repository>
+    <snapshotRepository>
+      <uniqueVersion>false</uniqueVersion>
+      <id>sonatype-joda-snapshot</id>
+      <name>Sonatype OSS snapshot repository</name>
+      <url>https://oss.sonatype.org/content/repositories/joda-snapshots</url>
+      <layout>default</layout>
+    </snapshotRepository>
+    <downloadUrl>https://oss.sonatype.org/content/repositories/joda-releases</downloadUrl>
+  </distributionManagement>
+
   <!-- ==================================================================== -->
   <profiles>
     <!-- Main deployment profile, activated by -Doss.repo -->
@@ -130,7 +684,7 @@
                   <classifier>classic</classifier>
                   <archive>
                     <manifestEntries>
-                      <Automatic-Module-Name>${joda.module.name}</Automatic-Module-Name>
+                      <Automatic-Module-Name>org.joda.convert</Automatic-Module-Name>
                     </manifestEntries>
                   </archive>
                   <excludes>
@@ -161,24 +715,52 @@
               </execution>
             </executions>
           </plugin>
+          <!-- Sign artifacts -->
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-gpg-plugin</artifactId>
+            <executions>
+              <execution>
+                <id>sign-artifacts</id>
+                <phase>verify</phase>
+                <goals>
+                  <goal>sign</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+          <!-- Use nexus plugin to directly release -->
+          <plugin>
+            <groupId>org.sonatype.plugins</groupId>
+            <artifactId>nexus-staging-maven-plugin</artifactId>
+            <version>${nexus-staging-maven-plugin.version}</version>
+            <extensions>true</extensions>
+            <configuration>
+              <nexusUrl>https://oss.sonatype.org/</nexusUrl>
+              <serverId>sonatype-joda-staging</serverId>
+              <autoReleaseAfterClose>${joda.nexus.auto.release}</autoReleaseAfterClose>
+              <keepStagingRepositoryOnCloseRuleFailure>true</keepStagingRepositoryOnCloseRuleFailure>
+              <stagingProgressTimeoutMinutes>20</stagingProgressTimeoutMinutes>
+            </configuration>
+          </plugin>
           <!-- Release dist files to GitHub -->
           <!-- This will create a tag on GitHub on deploy -->
           <!-- The release commit must have been pushed first -->
           <plugin>
             <groupId>de.jutzig</groupId>
             <artifactId>github-release-plugin</artifactId>
-            <version>1.4.0</version>
+            <version>${github-release-plugin.version}</version>
             <configuration>
               <releaseName>Release v${project.version}</releaseName>
-              <description>See the [change notes](https://www.joda.org/${joda.artifactId}/changes-report.html#a${project.version}) for more information.</description>
+              <description>See the [change notes](https://www.joda.org/joda-convert/changes-report.html#a${project.version}) for more information.</description>
               <tag>v${project.version}</tag>
               <overwriteArtifact>true</overwriteArtifact>
               <fileSets>
                 <fileSet>
                   <directory>${project.build.directory}</directory>
                   <includes>
-                    <include>${joda.artifactId}*-dist.tar.gz</include>
-                    <include>${joda.artifactId}*-dist.zip</include>
+                    <include>joda-convert*-dist.tar.gz</include>
+                    <include>joda-convert*-dist.zip</include>
                   </includes>
                 </fileSet>
               </fileSets>
@@ -200,14 +782,63 @@
 
   <!-- ==================================================================== -->
   <properties>
-    <!-- Parent pom.xml control -->
+    <!-- Common control parameters -->
     <joda.osgi.packages>org.joda.convert.*</joda.osgi.packages>
-    <joda.module.name>org.joda.convert</joda.module.name>
-    <joda.artifactId>joda-convert</joda.artifactId>
-    <!-- Set to Java 6 -->
-    <joda.release.version>6</joda.release.version>
+    <joda.osgi.require.capability>osgi.ee;filter:="(&amp;(osgi.ee=JavaSE)(version=${maven.compiler.source}))"</joda.osgi.require.capability>
+    <joda.nexus.auto.release>true</joda.nexus.auto.release>
+
+    <!-- Plugin version numbers -->
+    <maven-assembly-plugin.version>3.4.2</maven-assembly-plugin.version>
+    <maven-bundle-plugin.version>5.1.8</maven-bundle-plugin.version>
+    <maven-changes-plugin.version>2.12.1</maven-changes-plugin.version>
+    <maven-checkstyle-plugin.version>3.1.1</maven-checkstyle-plugin.version>
+    <maven-clean-plugin.version>3.2.0</maven-clean-plugin.version>
+    <maven-compiler-plugin.version>3.10.1</maven-compiler-plugin.version>
+    <maven-deploy-plugin.version>3.0.0</maven-deploy-plugin.version>
+    <maven-dependency-plugin.version>3.3.0</maven-dependency-plugin.version>
+    <maven-enforcer-plugin.version>3.1.0</maven-enforcer-plugin.version>
+    <maven-gpg-plugin.version>3.0.1</maven-gpg-plugin.version>
+    <maven-install-plugin.version>3.0.1</maven-install-plugin.version>
+    <maven-jar-plugin.version>3.2.2</maven-jar-plugin.version>
+    <maven-javadoc-plugin.version>3.4.1</maven-javadoc-plugin.version>
+    <maven-jxr-plugin.version>3.3.0</maven-jxr-plugin.version>
+    <maven-plugin-plugin.version>3.6.4</maven-plugin-plugin.version>
+    <maven-pmd-plugin.version>3.19.0</maven-pmd-plugin.version>
+    <maven-project-info-reports-plugin.version>3.4.1</maven-project-info-reports-plugin.version>
+    <maven-release-plugin.version>2.5.3</maven-release-plugin.version>
+    <maven-repository-plugin.version>2.4</maven-repository-plugin.version>
+    <maven-resources-plugin.version>3.3.0</maven-resources-plugin.version>
+    <maven-site-plugin.version>3.12.1</maven-site-plugin.version>
+    <maven-source-plugin.version>3.2.1</maven-source-plugin.version>
+    <maven-surefire-plugin.version>3.0.0-M7</maven-surefire-plugin.version>
+    <maven-surefire-report-plugin.version>3.0.0-M7</maven-surefire-report-plugin.version>
+    <maven-toolchains-plugin.version>3.1.0</maven-toolchains-plugin.version>
+    <github-api.version>1.308</github-api.version>
+    <github-release-plugin.version>1.4.0</github-release-plugin.version>
+    <jacoco-maven-plugin.version>0.8.8</jacoco-maven-plugin.version>
+    <nexus-staging-maven-plugin.version>1.6.13</nexus-staging-maven-plugin.version>
+    <revapi-maven-plugin.version>0.11.1</revapi-maven-plugin.version>
+    <revapi-java.version>0.15.1</revapi-java.version>
+    <spotbugs-maven-plugin.version>4.7.1.1</spotbugs-maven-plugin.version>
+
+    <!-- Properties for maven-compiler-plugin -->
     <maven.compiler.compilerVersion>1.6</maven.compiler.compilerVersion>
     <maven.compiler.source>1.6</maven.compiler.source>
     <maven.compiler.target>1.6</maven.compiler.target>
+    <maven.compiler.fork>true</maven.compiler.fork>
+
+    <!-- Properties for maven-javadoc-plugin -->
+    <author>false</author>
+    <notimestamp>true</notimestamp>
+    <doclint>none</doclint>
+
+    <!-- Properties for maven-checkstyle-plugin -->
+    <checkstyle.version>8.45.1</checkstyle.version>
+    <checkstyle.config.location>src/main/checkstyle/checkstyle.xml</checkstyle.config.location>
+    <linkXRef>false</linkXRef>
+
+    <!-- Other properties -->
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
   </properties>
 </project>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index ee3b3e4..2ade1b0 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -7,6 +7,15 @@
 
   <body>
     <!-- types are add, fix, remove, update -->
+    <release version="2.2.3" date="2023-01-15" description="Version 2.2.3">
+      <action dev="jodastephen" type="add">
+        Support classes that define `@FromString` but not `@ToString`.
+        This can be used to leniently parse classes where the format used to be a Joda-Convert class.
+      </action>
+      <action dev="jodastephen" type="update">
+        Switch LGTM to CodeQL.
+      </action>
+    </release>
     <release version="2.2.2" date="2021-12-15" description="Version 2.2.2">
       <action dev="jodastephen" type="fix">
         Fix deserialization of array classes.
diff --git a/src/main/checkstyle/checkstyle.xml b/src/main/checkstyle/checkstyle.xml
index 5774a16..beb269d 100644
--- a/src/main/checkstyle/checkstyle.xml
+++ b/src/main/checkstyle/checkstyle.xml
@@ -3,8 +3,8 @@
 
 <module name="Checker">
   <property name="severity" value="warning"/>
+  <property name="tabWidth" value="4"/>
   <module name="TreeWalker">
-    <property name="tabWidth" value="4"/>
     <module name="ConstantName">
       <property name="format" value="^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$|^[a-z][a-zA-Z0-9]*$"/>
     </module>
@@ -29,13 +29,7 @@
       <property name="scope" value="protected"/>
     </module>
     <module name="JavadocMethod">
-      <property name="scope" value="protected"/>
-      <property name="allowUndeclaredRTE" value="true"/>
-      <property name="allowMissingThrowsTags" value="true"/>
-      <property name="allowMissingJavadoc" value="true"/>
-      <property name="allowMissingPropertyJavadoc" value="true"/>
-      <property name="logLoadErrors" value="true"/>
-      <property name="suppressLoadErrors" value="true"/>
+      <property name="accessModifiers" value="public,protected"/>
     </module>
     <module name="JavadocVariable">
       <property name="scope" value="protected"/>
@@ -43,11 +37,6 @@
     <module name="LeftCurly">
       <property name="severity" value="error"/>
     </module>
-    <module name="LineLength">
-      <property name="ignorePattern" value="^ *\* *[^ ]+$"/>
-      <property name="max" value="200"/>
-      <property name="tabWidth" value="2"/>
-    </module>
     <module name="LocalFinalVariableName"/>
     <module name="LocalVariableName"/>
     <module name="MemberName">
@@ -57,6 +46,9 @@
       <property name="max" value="300"/>
     </module>
     <module name="MethodName"/>
+    <module name="MissingJavadocMethodCheck">
+      <property name="allowMissingPropertyJavadoc" value="true"/>
+    </module>
     <module name="ModifierOrder">
       <property name="severity" value="error"/>
     </module>
@@ -79,7 +71,6 @@
     </module>
     <module name="ParenPad"/>
     <module name="RedundantImport"/>
-    <module name="RedundantModifier"/>
     <module name="RightCurly">
       <property name="severity" value="error"/>
     </module>
@@ -138,5 +129,9 @@
     <property name="eachLine" value="true"/>
     <property name="severity" value="error"/>
   </module>
+  <module name="LineLength">
+    <property name="ignorePattern" value="^ *\* *[^ ]+$"/>
+    <property name="max" value="200"/>
+  </module>
   <module name="NewlineAtEndOfFile"/>
 </module>
diff --git a/src/main/java/org/joda/convert/AnnotationStringConverterFactory.java b/src/main/java/org/joda/convert/AnnotationStringConverterFactory.java
index 13c6bb0..d183918 100644
--- a/src/main/java/org/joda/convert/AnnotationStringConverterFactory.java
+++ b/src/main/java/org/joda/convert/AnnotationStringConverterFactory.java
@@ -30,7 +30,7 @@ final class AnnotationStringConverterFactory implements StringConverterFactory {
     /**
      * Singleton instance.
      */
-    static final StringConverterFactory INSTANCE = new AnnotationStringConverterFactory();
+    static final AnnotationStringConverterFactory INSTANCE = new AnnotationStringConverterFactory();
 
     /**
      * Restricted constructor.
@@ -59,22 +59,48 @@ final class AnnotationStringConverterFactory implements StringConverterFactory {
      * @return the converter, not null
      * @throws RuntimeException if none found
      */
-    private <T> StringConverter<T> findAnnotatedConverter(final Class<T> cls) {
+    private <T> StringConverter<T> findAnnotatedConverter(Class<T> cls) {
         Method toString = findToStringMethod(cls);  // checks superclasses
         if (toString == null) {
             return null;
         }
-        MethodConstructorStringConverter<T> con = findFromStringConstructor(cls, toString);
-        MethodsStringConverter<T> mth = findFromStringMethod(cls, toString, con == null);  // optionally checks superclasses
-        if (con == null && mth == null) {
+        TypedFromStringConverter<T> fromString = findAnnotatedFromStringConverter(cls);
+        if (fromString == null) {
             throw new IllegalStateException("Class annotated with @ToString but not with @FromString: " + cls.getName());
         }
+        return new ReflectionStringConverter<T>(cls, toString, fromString);
+    }
+
+    /**
+     * Finds a from-string converter by type.
+     * 
+     * @param <T>  the type of the converter
+     * @param cls  the type to lookup, not null
+     * @return the converter, null if not found
+     * @throws RuntimeException (or subclass) if source code is invalid
+     */
+    <T> TypedFromStringConverter<T> findFromStringConverter(Class<T> cls) {
+        return findAnnotatedFromStringConverter(cls);  // capture generics
+    }
+
+    /**
+     * Finds a from-string converter.
+     * 
+     * @param <T>  the type of the converter
+     * @param cls  the class to find a method for, not null
+     * @return the converter, null if not found
+     * @throws RuntimeException if none found
+     */
+    private <T> TypedFromStringConverter<T> findAnnotatedFromStringConverter(Class<T> cls) {
+        TypedFromStringConverter<T> con = findFromStringConstructor(cls);
+        TypedFromStringConverter<T> mth = findFromStringMethod(cls, con == null);  // optionally checks superclasses
         if (con != null && mth != null) {
             throw new IllegalStateException("Both method and constructor are annotated with @FromString: " + cls.getName());
         }
         return (con != null ? con : mth);
     }
 
+    //-------------------------------------------------------------------------
     /**
      * Finds the conversion method.
      * 
@@ -121,16 +147,16 @@ final class AnnotationStringConverterFactory implements StringConverterFactory {
         return matched;
     }
 
+    //-------------------------------------------------------------------------
     /**
      * Finds the conversion method.
      * 
      * @param <T>  the type of the converter
      * @param cls  the class to find a method for, not null
-     * @param toString  the toString method, not null
      * @return the method to call, null means none found
      * @throws RuntimeException if invalid
      */
-    private <T> MethodConstructorStringConverter<T> findFromStringConstructor(Class<T> cls, Method toString) {
+    private <T> TypedFromStringConverter<T> findFromStringConstructor(Class<T> cls) {
         Constructor<T> con;
         try {
             con = cls.getDeclaredConstructor(String.class);
@@ -145,25 +171,24 @@ final class AnnotationStringConverterFactory implements StringConverterFactory {
         if (fromString == null) {
             return null;
         }
-        return new MethodConstructorStringConverter<T>(cls, toString, con);
+        return new ConstructorFromStringConverter<T>(cls, con);
     }
 
     /**
      * Finds the conversion method.
      * 
      * @param cls  the class to find a method for, not null
-     * @param toString  the toString method, not null
      * @param searchSuperclasses  whether to search superclasses
      * @return the method to call, null means not found
      * @throws RuntimeException if invalid
      */
-    private <T> MethodsStringConverter<T> findFromStringMethod(Class<T> cls, Method toString, boolean searchSuperclasses) {
+    private <T> TypedFromStringConverter<T> findFromStringMethod(Class<T> cls, boolean searchSuperclasses) {
         // find in superclass hierarchy
         Class<?> loopCls = cls;
         while (loopCls != null) {
             Method fromString = findFromString(loopCls);
             if (fromString != null) {
-                return new MethodsStringConverter<T>(cls, toString, fromString, loopCls);
+                return new MethodFromStringConverter<T>(cls, fromString, loopCls);
             }
             if (searchSuperclasses == false) {
                 break;
@@ -171,7 +196,7 @@ final class AnnotationStringConverterFactory implements StringConverterFactory {
             loopCls = loopCls.getSuperclass();
         }
         // find in immediate parent interfaces
-        MethodsStringConverter<T> matched = null;
+        TypedFromStringConverter<T> matched = null;
         if (searchSuperclasses) {
             for (Class<?> loopIfc : eliminateEnumSubclass(cls).getInterfaces()) {
                 Method fromString = findFromString(loopIfc);
@@ -180,7 +205,7 @@ final class AnnotationStringConverterFactory implements StringConverterFactory {
                         throw new IllegalStateException("Two different interfaces are annotated with " +
                             "@FromString or @FromStringFactory: " + cls.getName());
                     }
-                    matched = new MethodsStringConverter<T>(cls, toString, fromString, loopIfc);
+                    matched = new MethodFromStringConverter<T>(cls, fromString, loopIfc);
                 }
             }
         }
diff --git a/src/main/java/org/joda/convert/MethodConstructorStringConverter.java b/src/main/java/org/joda/convert/ConstructorFromStringConverter.java
similarity index 75%
rename from src/main/java/org/joda/convert/MethodConstructorStringConverter.java
rename to src/main/java/org/joda/convert/ConstructorFromStringConverter.java
index c0cd2f6..4fd56e6 100644
--- a/src/main/java/org/joda/convert/MethodConstructorStringConverter.java
+++ b/src/main/java/org/joda/convert/ConstructorFromStringConverter.java
@@ -1,87 +1,76 @@
-/*
- *  Copyright 2010-present Stephen Colebourne
- *
- *  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 org.joda.convert;
-
-import java.lang.reflect.Constructor;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-
-/**
- * Conversion to and from a string using a toString method and a fromString constructor.
- * <p>
- * The toString method must meet the following signature:<br />
- * {@code String anyName()} on Class T.
- * <p>
- * The fromString constructor must take a single {@code String} parameter.
- * <p>
- * MethodConstructorStringConverter is thread-safe and immutable.
- * 
- * @param <T>  the type of the converter
- */
-final class MethodConstructorStringConverter<T> extends ReflectionStringConverter<T> {
-
-    /** Conversion from a string. */
-    private final Constructor<T> fromString;
-
-    /**
-     * Creates an instance using a method and a constructor.
-     * @param cls  the class this converts for, not null
-     * @param toString  the toString method, not null
-     * @param fromString  the fromString method, not null
-     * @throws RuntimeException (or subclass) if the method signatures are invalid
-     */
-    MethodConstructorStringConverter(Class<T> cls, Method toString, Constructor<T> fromString) {
-        super(cls, toString);
-        if (cls.isInterface() || Modifier.isAbstract(cls.getModifiers()) || cls.isLocalClass() || cls.isMemberClass()) {
-            throw new IllegalArgumentException("FromString constructor must be on an instantiable class: " + fromString);
-        }
-        if (fromString.getDeclaringClass() != cls) {
-            throw new IllegalStateException("FromString constructor must be defined on specified class: " + fromString);
-        }
-        this.fromString = fromString;
-    }
-
-    //-----------------------------------------------------------------------
-    /**
-     * Converts the {@code String} to an object.
-     * @param cls  the class to convert to, not null
-     * @param str  the string to convert, not null
-     * @return the converted object, may be null but generally not
-     */
-    @Override
-    public T convertFromString(Class<? extends T> cls, String str) {
-        try {
-            return fromString.newInstance(str);
-        } catch (IllegalAccessException ex) {
-            throw new IllegalStateException("Constructor is not accessible: " + fromString);
-        } catch (InstantiationException ex) {
-            throw new IllegalStateException("Constructor is not valid: " + fromString);
-        } catch (InvocationTargetException ex) {
-            if (ex.getCause() instanceof RuntimeException) {
-                throw (RuntimeException) ex.getCause();
-            }
-            throw new RuntimeException(ex.getMessage(), ex.getCause());
-        }
-    }
-
-    //-------------------------------------------------------------------------
-    @Override
-    public Class<?> getEffectiveType() {
-        return fromString.getDeclaringClass();
-    }
-
-}
+/*
+ *  Copyright 2010-present Stephen Colebourne
+ *
+ *  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 org.joda.convert;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+
+/**
+ * Conversion from a string using a fromString constructor.
+ * <p>
+ * The fromString constructor must take a single {@code String} parameter.
+ * <p>
+ * ConstructorFromStringConverter is thread-safe and immutable.
+ * 
+ * @param <T>  the type of the converter
+ */
+final class ConstructorFromStringConverter<T> implements TypedFromStringConverter<T> {
+
+    /** Conversion from a string. */
+    private final Constructor<T> fromString;
+
+    /**
+     * Creates an instance using a method and a constructor.
+     * 
+     * @param cls  the class this converts for, not null
+     * @param fromString  the fromString method, not null
+     * @throws RuntimeException (or subclass) if the method signatures are invalid
+     */
+    ConstructorFromStringConverter(Class<T> cls, Constructor<T> fromString) {
+        if (cls.isInterface() || Modifier.isAbstract(cls.getModifiers()) || cls.isLocalClass() || cls.isMemberClass()) {
+            throw new IllegalArgumentException("FromString constructor must be on an instantiable class: " + fromString);
+        }
+        if (fromString.getDeclaringClass() != cls) {
+            throw new IllegalStateException("FromString constructor must be defined on specified class: " + fromString);
+        }
+        this.fromString = fromString;
+    }
+
+    //-----------------------------------------------------------------------
+    @Override
+    public T convertFromString(Class<? extends T> cls, String str) {
+        try {
+            return fromString.newInstance(str);
+        } catch (IllegalAccessException ex) {
+            throw new IllegalStateException("Constructor is not accessible: " + fromString);
+        } catch (InstantiationException ex) {
+            throw new IllegalStateException("Constructor is not valid: " + fromString);
+        } catch (InvocationTargetException ex) {
+            if (ex.getCause() instanceof RuntimeException) {
+                throw (RuntimeException) ex.getCause();
+            }
+            throw new RuntimeException(ex.getMessage(), ex.getCause());
+        }
+    }
+
+    //-------------------------------------------------------------------------
+    @Override
+    public Class<?> getEffectiveType() {
+        return fromString.getDeclaringClass();
+    }
+
+}
diff --git a/src/main/java/org/joda/convert/MethodsStringConverter.java b/src/main/java/org/joda/convert/MethodFromStringConverter.java
similarity index 79%
rename from src/main/java/org/joda/convert/MethodsStringConverter.java
rename to src/main/java/org/joda/convert/MethodFromStringConverter.java
index 455dfd0..21789f7 100644
--- a/src/main/java/org/joda/convert/MethodsStringConverter.java
+++ b/src/main/java/org/joda/convert/MethodFromStringConverter.java
@@ -1,95 +1,85 @@
-/*
- *  Copyright 2010-present Stephen Colebourne
- *
- *  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 org.joda.convert;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-
-/**
- * Conversion to and from a string using two methods.
- * <p>
- * The toString method must meet the following signature:<br />
- * {@code String anyName()} on Class T.
- * <p>
- * The fromString method must meet the following signature:<br />
- * {@code static T anyName(String)} on any class.
- * <p>
- * MethodsStringConverter is thread-safe and immutable.
- * 
- * @param <T>  the type of the converter
- */
-final class MethodsStringConverter<T> extends ReflectionStringConverter<T> {
-
-    /** Conversion from a string. */
-    private final Method fromString;
-    /** Effective type. */
-    private final Class<?> effectiveType;
-
-    /**
-     * Creates an instance using two methods.
-     * @param cls  the class this converts for, not null
-     * @param toString  the toString method, not null
-     * @param fromString  the fromString method, not null
-     * @throws RuntimeException (or subclass) if the method signatures are invalid
-     */
-    MethodsStringConverter(Class<T> cls, Method toString, Method fromString, Class<?> effectiveType) {
-        super(cls, toString);
-        if (Modifier.isStatic(fromString.getModifiers()) == false) {
-            throw new IllegalStateException("FromString method must be static: " + fromString);
-        }
-        if (fromString.getParameterTypes().length != 1) {
-            throw new IllegalStateException("FromString method must have one parameter: " + fromString);
-        }
-        Class<?> param = fromString.getParameterTypes()[0];
-        if (param != String.class && param != CharSequence.class) {
-            throw new IllegalStateException("FromString method must take a String or CharSequence: " + fromString);
-        }
-        if (fromString.getReturnType().isAssignableFrom(cls) == false && cls.isAssignableFrom(fromString.getReturnType()) == false) {
-            throw new IllegalStateException("FromString method must return specified class or a supertype: " + fromString);
-        }
-        this.fromString = fromString;
-        this.effectiveType = effectiveType;
-    }
-
-    //-----------------------------------------------------------------------
-    /**
-     * Converts the {@code String} to an object.
-     * @param cls  the class to convert to, not null
-     * @param str  the string to convert, not null
-     * @return the converted object, may be null but generally not
-     */
-    @Override
-    public T convertFromString(Class<? extends T> cls, String str) {
-        try {
-            return cls.cast(fromString.invoke(null, str));
-        } catch (IllegalAccessException ex) {
-            throw new IllegalStateException("Method is not accessible: " + fromString);
-        } catch (InvocationTargetException ex) {
-            if (ex.getCause() instanceof RuntimeException) {
-                throw (RuntimeException) ex.getCause();
-            }
-            throw new RuntimeException(ex.getMessage(), ex.getCause());
-        }
-    }
-
-    //-------------------------------------------------------------------------
-    @Override
-    public Class<?> getEffectiveType() {
-        return effectiveType;
-    }
-
-}
+/*
+ *  Copyright 2010-present Stephen Colebourne
+ *
+ *  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 org.joda.convert;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+/**
+ * Conversion from a string using a static method.
+ * <p>
+ * The fromString method must meet the following signature:<br />
+ * {@code static T anyName(String)} on any class.
+ * <p>
+ * MethodFromStringConverter is thread-safe and immutable.
+ * 
+ * @param <T>  the type of the converter
+ */
+final class MethodFromStringConverter<T> implements TypedFromStringConverter<T> {
+
+    /** Conversion from a string. */
+    private final Method fromString;
+    /** Effective type. */
+    private final Class<?> effectiveType;
+
+    /**
+     * Creates an instance using two methods.
+     * 
+     * @param cls  the class this converts for, not null
+     * @param fromString  the fromString method, not null
+     * @throws RuntimeException (or subclass) if the method signatures are invalid
+     */
+    MethodFromStringConverter(Class<T> cls, Method fromString, Class<?> effectiveType) {
+        if (Modifier.isStatic(fromString.getModifiers()) == false) {
+            throw new IllegalStateException("FromString method must be static: " + fromString);
+        }
+        if (fromString.getParameterTypes().length != 1) {
+            throw new IllegalStateException("FromString method must have one parameter: " + fromString);
+        }
+        Class<?> param = fromString.getParameterTypes()[0];
+        if (param != String.class && param != CharSequence.class) {
+            throw new IllegalStateException("FromString method must take a String or CharSequence: " + fromString);
+        }
+        if (fromString.getReturnType().isAssignableFrom(cls) == false && cls.isAssignableFrom(fromString.getReturnType()) == false) {
+            throw new IllegalStateException("FromString method must return specified class or a supertype: " + fromString);
+        }
+        this.fromString = fromString;
+        this.effectiveType = effectiveType;
+    }
+
+    //-----------------------------------------------------------------------
+    @Override
+    public T convertFromString(Class<? extends T> cls, String str) {
+        try {
+            return cls.cast(fromString.invoke(null, str));
+        } catch (IllegalAccessException ex) {
+            throw new IllegalStateException("Method is not accessible: " + fromString);
+        } catch (InvocationTargetException ex) {
+            if (ex.getCause() instanceof RuntimeException) {
+                throw (RuntimeException) ex.getCause();
+            }
+            throw new RuntimeException(ex.getMessage(), ex.getCause());
+        }
+    }
+
+    //-------------------------------------------------------------------------
+    @Override
+    public Class<?> getEffectiveType() {
+        return effectiveType;
+    }
+
+}
diff --git a/src/main/java/org/joda/convert/ReflectionStringConverter.java b/src/main/java/org/joda/convert/ReflectionStringConverter.java
index 702f652..965d2bd 100644
--- a/src/main/java/org/joda/convert/ReflectionStringConverter.java
+++ b/src/main/java/org/joda/convert/ReflectionStringConverter.java
@@ -28,20 +28,24 @@ import java.lang.reflect.Method;
  * 
  * @param <T>  the type of the converter
  */
-abstract class ReflectionStringConverter<T> implements TypedStringConverter<T> {
+final class ReflectionStringConverter<T> implements TypedStringConverter<T> {
 
     /** The converted class. */
     private final Class<T> cls;
     /** Conversion to a string. */
     private final Method toString;
+    /** Conversion from a string, package-scoped for testing. */
+    final TypedFromStringConverter<T> fromString;
 
     /**
      * Creates an instance using two methods.
-     * @param cls  the class this converts for, not null
+     * 
+     * @param cls  the class this converts for, null creates a from-string converter
      * @param toString  the toString method, not null
+     * @param fromString  the fromString converter, not null
      * @throws RuntimeException (or subclass) if the method signatures are invalid
      */
-    ReflectionStringConverter(Class<T> cls, Method toString) {
+    ReflectionStringConverter(Class<T> cls, Method toString, TypedFromStringConverter<T> fromString) {
         if (toString.getParameterTypes().length != 0) {
             throw new IllegalStateException("ToString method must have no parameters: " + toString);
         }
@@ -50,14 +54,10 @@ abstract class ReflectionStringConverter<T> implements TypedStringConverter<T> {
         }
         this.cls = cls;
         this.toString = toString;
+        this.fromString = fromString;
     }
 
     //-----------------------------------------------------------------------
-    /**
-     * Converts the object to a {@code String}.
-     * @param object  the object to convert, not null
-     * @return the converted string, may be null but generally not
-     */
     @Override
     public String convertToString(T object) {
         try {
@@ -72,6 +72,16 @@ abstract class ReflectionStringConverter<T> implements TypedStringConverter<T> {
         }
     }
 
+    @Override
+    public T convertFromString(Class<? extends T> cls, String str) {
+        return fromString.convertFromString(cls, str);
+    }
+
+    @Override
+    public Class<?> getEffectiveType() {
+        return fromString.getEffectiveType();
+    }
+
     //-----------------------------------------------------------------------
     @Override
     public String toString() {
diff --git a/src/main/java/org/joda/convert/StringConvert.java b/src/main/java/org/joda/convert/StringConvert.java
index c262605..921b72d 100644
--- a/src/main/java/org/joda/convert/StringConvert.java
+++ b/src/main/java/org/joda/convert/StringConvert.java
@@ -92,6 +92,10 @@ public final class StringConvert {
      * The cache of converters.
      */
     private final ConcurrentMap<Class<?>, TypedStringConverter<?>> registered = new ConcurrentHashMap<Class<?>, TypedStringConverter<?>>();
+    /**
+     * The cache of from-strings.
+     */
+    private final ConcurrentMap<Class<?>, FromStringConverter<?>> fromStrings = new ConcurrentHashMap<Class<?>, FromStringConverter<?>>();
 
     //-----------------------------------------------------------------------
     /**
@@ -221,7 +225,7 @@ public final class StringConvert {
             // if we have created a read edge, or if we are on the classpath, this will succeed
             loadType("com.google.common.reflect.TypeToken");
             @SuppressWarnings("unchecked")
-            Class<?> cls = (Class<TypedStringConverter<?>>) loadType("org.joda.convert.TypeTokenStringConverter");
+            Class<?> cls = loadType("org.joda.convert.TypeTokenStringConverter");
             TypedStringConverter<?> conv = (TypedStringConverter<?>) cls.getDeclaredConstructor().newInstance();
             registered.put(conv.getEffectiveType(), conv);
 
@@ -239,17 +243,17 @@ public final class StringConvert {
         try {
             loadType("java.util.OptionalInt");
             @SuppressWarnings("unchecked")
-            Class<?> cls1 = (Class<TypedStringConverter<?>>) loadType("org.joda.convert.OptionalIntStringConverter");
+            Class<?> cls1 = loadType("org.joda.convert.OptionalIntStringConverter");
             TypedStringConverter<?> conv1 = (TypedStringConverter<?>) cls1.getDeclaredConstructor().newInstance();
             registered.put(conv1.getEffectiveType(), conv1);
 
             @SuppressWarnings("unchecked")
-            Class<?> cls2 = (Class<TypedStringConverter<?>>) loadType("org.joda.convert.OptionalLongStringConverter");
+            Class<?> cls2 = loadType("org.joda.convert.OptionalLongStringConverter");
             TypedStringConverter<?> conv2 = (TypedStringConverter<?>) cls2.getDeclaredConstructor().newInstance();
             registered.put(conv2.getEffectiveType(), conv2);
 
             @SuppressWarnings("unchecked")
-            Class<?> cls3 = (Class<TypedStringConverter<?>>) loadType("org.joda.convert.OptionalDoubleStringConverter");
+            Class<?> cls3 = loadType("org.joda.convert.OptionalDoubleStringConverter");
             TypedStringConverter<?> conv3 = (TypedStringConverter<?>) cls3.getDeclaredConstructor().newInstance();
             registered.put(conv3.getEffectiveType(), conv3);
 
@@ -430,7 +434,7 @@ public final class StringConvert {
     /**
      * Converts the specified object from a {@code String}.
      * <p>
-     * This uses {@link #findConverter} to provide the converter.
+     * This uses {@link #findFromStringConverter} to provide the converter.
      * 
      * @param <T>  the type to convert to
      * @param cls  the class to convert to, not null
@@ -442,7 +446,7 @@ public final class StringConvert {
         if (str == null) {
             return null;
         }
-        StringConverter<T> conv = findConverter(cls);
+        FromStringConverter<T> conv = findFromStringConverter(cls);
         return conv.convertFromString(cls, str);
     }
 
@@ -576,13 +580,31 @@ public final class StringConvert {
     }
 
     /**
-     * Finds a converter searching registered and annotated.
+     * Finds a suitable from-string converter for the type.
+     * <p>
+     * This returns an instance of {@code FromStringConverter} for the specified class.
+     * In most cases this is identical to {@link #findConverter(Class)}.
+     * However, it is permitted to have a {@code FromString} annotation without a {@code ToString} annotation,
+     * and this method catches that use case.
      * 
      * @param <T>  the type of the converter
-     * @param cls  the class to find a method for, not null
-     * @return the converter, null if no converter
-     * @throws RuntimeException if invalid
+     * @param cls  the class to find a converter for, not null
+     * @return the converter, not null
+     * @throws RuntimeException (or subclass) if no converter found
      */
+    @SuppressWarnings("unchecked")
+    public <T> FromStringConverter<T> findFromStringConverter(final Class<T> cls) {
+        TypedStringConverter<T> converter = findConverterQuiet(cls);
+        if (converter == null) {
+            FromStringConverter<T> fromStringConverter = (FromStringConverter<T>) fromStrings.get(cls);
+            if (fromStringConverter == null) {
+                throw new IllegalStateException("No registered converter found: " + cls);
+            }
+            return fromStringConverter;
+        }
+        return converter;
+    }
+
     @SuppressWarnings("unchecked")
     private <T> TypedStringConverter<T> findConverterQuiet(final Class<T> cls) {
         if (cls == null) {
@@ -594,13 +616,18 @@ public final class StringConvert {
         }
         if (conv == null) {
             try {
-                conv = findAnyConverter(cls);
+                conv = lookupConverter(cls);
             } catch (RuntimeException ex) {
                 registered.putIfAbsent(cls, CACHED_NULL);
                 throw ex;
             }
             if (conv == null) {
                 registered.putIfAbsent(cls, CACHED_NULL);
+                // search for from-string only converters now, so that our cache is accurate for all kinds of converter
+                TypedFromStringConverter<T> fromString = AnnotationStringConverterFactory.INSTANCE.findFromStringConverter(cls);
+                if (fromString != null) {
+                    fromStrings.put(cls, fromString);
+                }
                 return null;
             }
             registered.putIfAbsent(cls, conv);
@@ -609,7 +636,7 @@ public final class StringConvert {
     }
 
     /**
-     * Finds a converter searching registered and annotated.
+     * Lookup a converter searching registered and annotated.
      * 
      * @param <T>  the type of the converter
      * @param cls  the class to find a method for, not null
@@ -617,7 +644,7 @@ public final class StringConvert {
      * @throws RuntimeException if invalid
      */
     @SuppressWarnings("unchecked")
-    private <T> TypedStringConverter<T> findAnyConverter(final Class<T> cls) {
+    private <T> TypedStringConverter<T> lookupConverter(final Class<T> cls) {
         // check factories
         for (StringConverterFactory factory : factories) {
             StringConverter<T> factoryConv = (StringConverter<T>) factory.findConverter(cls);
@@ -748,7 +775,8 @@ public final class StringConvert {
         }
         Method toString = findToStringMethod(cls, toStringMethodName);
         Method fromString = findFromStringMethod(cls, fromStringMethodName);
-        MethodsStringConverter<T> converter = new MethodsStringConverter<T>(cls, toString, fromString, cls);
+        TypedFromStringConverter<T> fromStringConverter = new MethodFromStringConverter<T>(cls, fromString, cls);
+        ReflectionStringConverter<T> converter = new ReflectionStringConverter<T>(cls, toString, fromStringConverter);
         registered.putIfAbsent(cls, converter);
     }
 
@@ -782,7 +810,8 @@ public final class StringConvert {
         }
         Method toString = findToStringMethod(cls, toStringMethodName);
         Constructor<T> fromString = findFromStringConstructorByType(cls);
-        MethodConstructorStringConverter<T> converter = new MethodConstructorStringConverter<T>(cls, toString, fromString);
+        TypedFromStringConverter<T> fromStringConverter = new ConstructorFromStringConverter<T>(cls, fromString);
+        ReflectionStringConverter<T> converter = new ReflectionStringConverter<T>(cls, toString, fromStringConverter);
         registered.putIfAbsent(cls, converter);
     }
 
diff --git a/src/main/java/org/joda/convert/TypedFromStringConverter.java b/src/main/java/org/joda/convert/TypedFromStringConverter.java
new file mode 100644
index 0000000..0a91bb1
--- /dev/null
+++ b/src/main/java/org/joda/convert/TypedFromStringConverter.java
@@ -0,0 +1,40 @@
+/*
+ *  Copyright 2010-present Stephen Colebourne
+ *
+ *  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 org.joda.convert;
+
+/**
+ * Interface defining conversion from a {@code String} together with the type.
+ * <p>
+ * TypedFromStringConverter is an interface and must be implemented with care.
+ * Implementations must be immutable and thread-safe.
+ * 
+ * @param <T>  the type of the converter
+ * @since 2.3
+ */
+interface TypedFromStringConverter<T> extends FromStringConverter<T> {
+
+    /**
+     * Gets the effective type that the converter works on.
+     * <p>
+     * For example, if a class declares the {@code FromString} and  {@code ToString}
+     * then the effective type of the converter is that class. If a subclass is
+     * queried for a converter, then the effective type is that of the superclass.
+     * 
+     * @return the effective type
+     */
+    Class<?> getEffectiveType();
+
+}
diff --git a/src/main/java/org/joda/convert/TypedStringConverter.java b/src/main/java/org/joda/convert/TypedStringConverter.java
index 6e31f21..5502e47 100644
--- a/src/main/java/org/joda/convert/TypedStringConverter.java
+++ b/src/main/java/org/joda/convert/TypedStringConverter.java
@@ -24,7 +24,7 @@ package org.joda.convert;
  * @param <T>  the type of the converter
  * @since 1.7
  */
-public interface TypedStringConverter<T> extends StringConverter<T> {
+public interface TypedStringConverter<T> extends StringConverter<T>, TypedFromStringConverter<T> {
 
     /**
      * Gets the effective type that the converter works on.
@@ -35,6 +35,7 @@ public interface TypedStringConverter<T> extends StringConverter<T> {
      * 
      * @return the effective type
      */
+    @Override
     Class<?> getEffectiveType();
 
 }
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index 402a088..1de081b 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -75,7 +75,7 @@ then the annotations are checked. If they are found, then the methods are called
 
 ## <i></i> Releases
 
-[Release 2.2.2](download.html) is the current latest release.
+[Release 2.2.3](download.html) is the current latest release.
 This release is considered stable and worthy of the 2.x tag.
 The v2.x releases are compatible with v1.x releases, with the exception that the direct Guava dependency is removed.
 
@@ -87,7 +87,7 @@ Available in [Maven Central](https://search.maven.org/search?q=g:org.joda%20AND%
 <dependency>
   <groupId>org.joda</groupId>
   <artifactId>joda-convert</artifactId>
-  <version>2.2.2</version>
+  <version>2.2.3</version>
 </dependency>
 ```
 
@@ -98,7 +98,7 @@ If you have problems with this, there is a "classic" variant you can use instead
 <dependency>
   <groupId>org.joda</groupId>
   <artifactId>joda-convert</artifactId>
-  <version>2.2.2</version>
+  <version>2.2.3</version>
   <classifier>classic</classifier>
 </dependency>
 ```
diff --git a/src/test/java/org/joda/convert/DistanceFromStringNoToString.java b/src/test/java/org/joda/convert/DistanceFromStringNoToString.java
index db05528..43811a5 100644
--- a/src/test/java/org/joda/convert/DistanceFromStringNoToString.java
+++ b/src/test/java/org/joda/convert/DistanceFromStringNoToString.java
@@ -29,14 +29,24 @@ public class DistanceFromStringNoToString {
 
     @FromString
     public DistanceFromStringNoToString(String amount) {
-        amount = amount.substring(0, amount.length() - 1);
-        this.amount = Integer.parseInt(amount);
+        this.amount = Integer.parseInt(amount.substring(0, amount.length() - 1));
     }
 
     public String print() {
         return amount + "m";
     }
 
+    @Override
+    public boolean equals(Object obj) {
+        return obj instanceof DistanceFromStringNoToString &&
+                ((DistanceFromStringNoToString) obj).amount == amount;
+    }
+
+    @Override
+    public int hashCode() {
+        return amount;
+    }
+
     @Override
     public String toString() {
         return "Distance[" + amount + "m]";
diff --git a/src/test/java/org/joda/convert/TestStringConvert.java b/src/test/java/org/joda/convert/TestStringConvert.java
index 18f7432..b9c9c7a 100644
--- a/src/test/java/org/joda/convert/TestStringConvert.java
+++ b/src/test/java/org/joda/convert/TestStringConvert.java
@@ -220,7 +220,7 @@ public class TestStringConvert {
         assertEquals("25m", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(DistanceMethodMethod.class, "25m").amount);
         TypedStringConverter<DistanceMethodMethod> conv = test.findTypedConverter(DistanceMethodMethod.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertSame(conv, test.findConverter(DistanceMethodMethod.class));
         assertEquals(DistanceMethodMethod.class, conv.getEffectiveType());
         assertEquals(true, conv.toString().startsWith("RefectionStringConverter"));
@@ -233,7 +233,7 @@ public class TestStringConvert {
         assertEquals("25m", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(DistanceMethodMethodCharSequence.class, "25m").amount);
         TypedStringConverter<DistanceMethodMethodCharSequence> conv = test.findTypedConverter(DistanceMethodMethodCharSequence.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertSame(conv, test.findConverter(DistanceMethodMethodCharSequence.class));
         assertEquals(DistanceMethodMethodCharSequence.class, conv.getEffectiveType());
         assertEquals(true, conv.toString().startsWith("RefectionStringConverter"));
@@ -246,7 +246,7 @@ public class TestStringConvert {
         assertEquals("25m", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(DistanceMethodConstructor.class, "25m").amount);
         TypedStringConverter<DistanceMethodConstructor> conv = test.findTypedConverter(DistanceMethodConstructor.class);
-        assertEquals(true, conv instanceof MethodConstructorStringConverter<?>);
+        assertFromStringConverter(conv, ConstructorFromStringConverter.class);
         assertSame(conv, test.findConverter(DistanceMethodConstructor.class));
         assertEquals(DistanceMethodConstructor.class, conv.getEffectiveType());
         assertEquals(true, conv.toString().startsWith("RefectionStringConverter"));
@@ -259,7 +259,7 @@ public class TestStringConvert {
         assertEquals("25m", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(DistanceMethodConstructorCharSequence.class, "25m").amount);
         TypedStringConverter<DistanceMethodConstructorCharSequence> conv = test.findTypedConverter(DistanceMethodConstructorCharSequence.class);
-        assertEquals(true, conv instanceof MethodConstructorStringConverter<?>);
+        assertFromStringConverter(conv, ConstructorFromStringConverter.class);
         assertSame(conv, test.findConverter(DistanceMethodConstructorCharSequence.class));
         assertEquals(DistanceMethodConstructorCharSequence.class, conv.getEffectiveType());
         assertEquals(true, conv.toString().startsWith("RefectionStringConverter"));
@@ -272,7 +272,7 @@ public class TestStringConvert {
         assertEquals("CODE", test.convertToString(d));
         assertEquals(d.code, test.convertFromString(HasCodeImpl.class, "CODE").code);
         TypedStringConverter<HasCodeImpl> conv = test.findTypedConverter(HasCodeImpl.class);
-        assertEquals(true, conv instanceof MethodConstructorStringConverter<?>);
+        assertFromStringConverter(conv, ConstructorFromStringConverter.class);
         assertSame(conv, test.findConverter(HasCodeImpl.class));
         assertEquals(HasCodeImpl.class, conv.getEffectiveType());
         assertEquals(true, conv.toString().startsWith("RefectionStringConverter"));
@@ -285,7 +285,7 @@ public class TestStringConvert {
         assertEquals("25m", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(SubMethodMethod.class, "25m").amount);
         TypedStringConverter<SubMethodMethod> conv = test.findTypedConverter(SubMethodMethod.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertEquals(SubMethodMethod.class, conv.getEffectiveType());
         assertSame(conv, test.findConverter(SubMethodMethod.class));
     }
@@ -297,7 +297,7 @@ public class TestStringConvert {
         assertEquals("25m", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(SubMethodConstructor.class, "25m").amount);
         TypedStringConverter<SubMethodConstructor> conv = test.findTypedConverter(SubMethodConstructor.class);
-        assertEquals(true, conv instanceof MethodConstructorStringConverter<?>);
+        assertFromStringConverter(conv, ConstructorFromStringConverter.class);
         assertEquals(SubMethodConstructor.class, conv.getEffectiveType());
         assertSame(conv, test.findConverter(SubMethodConstructor.class));
     }
@@ -309,7 +309,7 @@ public class TestStringConvert {
         assertEquals("25m", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(SuperFactorySuper.class, "25m").amount);
         TypedStringConverter<SuperFactorySuper> conv = test.findTypedConverter(SuperFactorySuper.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertEquals(SuperFactorySuper.class, conv.getEffectiveType());
         assertSame(conv, test.findConverter(SuperFactorySuper.class));
     }
@@ -323,7 +323,7 @@ public class TestStringConvert {
         assertEquals(d.amount, fromStr.amount);
         assertEquals(true, fromStr instanceof SuperFactorySub);
         TypedStringConverter<SuperFactorySub> conv = test.findTypedConverter(SuperFactorySub.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertEquals(SuperFactorySuper.class, conv.getEffectiveType());
         assertSame(conv, test.findConverter(SuperFactorySub.class));
     }
@@ -375,7 +375,7 @@ public class TestStringConvert {
         assertEquals("25m", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(DistanceWithFactory.class, "25m").amount);
         TypedStringConverter<DistanceWithFactory> conv = test.findTypedConverter(DistanceWithFactory.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertEquals(DistanceWithFactory.class, conv.getEffectiveType());
         assertSame(conv, test.findConverter(DistanceWithFactory.class));
         assertEquals(true, conv.toString().startsWith("RefectionStringConverter"));
@@ -388,7 +388,7 @@ public class TestStringConvert {
         assertEquals("25g", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(Test1Class.class, "25g").amount);
         TypedStringConverter<Test1Class> conv = test.findTypedConverter(Test1Class.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertEquals(Test1Class.class, conv.getEffectiveType());
         assertSame(conv, test.findConverter(Test1Class.class));
         assertEquals(true, conv.toString().startsWith("RefectionStringConverter"));
@@ -401,7 +401,7 @@ public class TestStringConvert {
         assertEquals("25g", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(Test2Class.class, "25g").amount);
         TypedStringConverter<Test2Class> conv = test.findTypedConverter(Test2Class.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertEquals(Test2Interface.class, conv.getEffectiveType());
         assertSame(conv, test.findConverter(Test2Class.class));
         assertEquals(true, conv.toString().startsWith("RefectionStringConverter"));
@@ -414,7 +414,7 @@ public class TestStringConvert {
         assertEquals("25g", test.convertToString(d));
         assertEquals("25g", test.convertFromString(Test2Interface.class, "25g").print());
         TypedStringConverter<Test2Interface> conv = test.findTypedConverter(Test2Interface.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertEquals(Test2Interface.class, conv.getEffectiveType());
         assertSame(conv, test.findConverter(Test2Interface.class));
         assertEquals(true, conv.toString().startsWith("RefectionStringConverter"));
@@ -427,7 +427,7 @@ public class TestStringConvert {
         assertEquals("25g", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(Test3Class.class, "25g").amount);
         TypedStringConverter<Test3Class> conv = test.findTypedConverter(Test3Class.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertSame(conv, test.findConverter(Test3Class.class));
         assertEquals(Test3SuperClass.class, conv.getEffectiveType());
         assertEquals(true, conv.toString().startsWith("RefectionStringConverter"));
@@ -524,6 +524,14 @@ public class TestStringConvert {
         test.findConverter(DistanceTwoFromStringMethodAnnotations.class);
     }
 
+    //-----------------------------------------------------------------------
+    @Test
+    public void test_convertFromString_annotatedFromStringNoToString() {
+        StringConvert test = new StringConvert();
+        DistanceFromStringNoToString result = test.convertFromString(DistanceFromStringNoToString.class, "2m");
+        assertEquals(new DistanceFromStringNoToString(2), result);
+    }
+
     //-----------------------------------------------------------------------
     @Test
     public void test_convert_Enum_overrideDefaultWithConverter() {
@@ -622,7 +630,7 @@ public class TestStringConvert {
         assertEquals("Distance[25m]", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(DistanceNoAnnotations.class, "25m").amount);
         StringConverter<DistanceNoAnnotations> conv = test.findConverter(DistanceNoAnnotations.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertSame(conv, test.findConverter(DistanceNoAnnotations.class));
     }
 
@@ -634,7 +642,7 @@ public class TestStringConvert {
         assertEquals("Distance[25m]", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(DistanceNoAnnotationsCharSequence.class, "25m").amount);
         StringConverter<DistanceNoAnnotationsCharSequence> conv = test.findConverter(DistanceNoAnnotationsCharSequence.class);
-        assertEquals(true, conv instanceof MethodsStringConverter<?>);
+        assertFromStringConverter(conv, MethodFromStringConverter.class);
         assertSame(conv, test.findConverter(DistanceNoAnnotationsCharSequence.class));
     }
 
@@ -695,7 +703,7 @@ public class TestStringConvert {
         assertEquals("Distance[25m]", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(DistanceNoAnnotationsCharSequence.class, "25m").amount);
         StringConverter<DistanceNoAnnotationsCharSequence> conv = test.findConverter(DistanceNoAnnotationsCharSequence.class);
-        assertEquals(true, conv instanceof MethodConstructorStringConverter<?>);
+        assertFromStringConverter(conv, ConstructorFromStringConverter.class);
         assertSame(conv, test.findConverter(DistanceNoAnnotationsCharSequence.class));
     }
 
@@ -707,7 +715,7 @@ public class TestStringConvert {
         assertEquals("Distance[25m]", test.convertToString(d));
         assertEquals(d.amount, test.convertFromString(DistanceNoAnnotations.class, "25m").amount);
         StringConverter<DistanceNoAnnotations> conv = test.findConverter(DistanceNoAnnotations.class);
-        assertEquals(true, conv instanceof MethodConstructorStringConverter<?>);
+        assertFromStringConverter(conv, ConstructorFromStringConverter.class);
         assertSame(conv, test.findConverter(DistanceNoAnnotations.class));
     }
 
@@ -747,4 +755,10 @@ public class TestStringConvert {
         assertEquals("StringConvert", new StringConvert().toString());
     }
 
+    private void assertFromStringConverter(StringConverter<?> conv, Class<?> expectedType) {
+        assertEquals(true, conv instanceof ReflectionStringConverter<?>);
+        Object obj = ((ReflectionStringConverter<?>) conv).fromString;
+        assertEquals(expectedType, obj.getClass());
+    }
+
 }

More details

Full run details

Historical runs