Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Adapting JSON Strings for Deserializing into C# Objects

4.00/5 (2 votes)
23 Jun 2019CPOL11 min read 11.8K  
Some JSON strings need a little help fitting into a C# object.

Introduction

Two recent projects have revolved around desrializing JSON strings returned by REST API endpoints into instances of C# classes. While most deserializations involved in the first project were routine, one in that project, and all of them in the most recent project. contained text that prevented their direct use as inputs to a JSON deserializer such as Newtonsoft.Json.JsonConvert.DeserializeObject<T>. For the first project, I devised a string extension method to apply pairs of substitutions to a string. For the more recent project, I refined it further, and added another method that performs a more complex set of string substitutions.

Background

To be suitable for deserialization into a C# object, a JSON string must look something like the following fragment.

JavaScript
{
    "Meta_Data": {

        "Information": "Daily Time Series with Splits and Dividend Events",
        "Symbol": "BA",
        "LastRefreshed": "2019-05-08 16:00:44",
        "OutputSize": "Compact",
        "TimeZone": "US/Eastern"
    },

    "Time_Series_Daily" : [
    {
        "Activity_Date": "2019-05-08",
        "Open": "357.7700",
        "High": "361.5200",
        "Low": "353.3300",
        "Close": "359.7500",
        "AdjustedClose": "359.7500",
        "Volume": "5911593",
        "DividendAmount": "0.0000",
        "SplitCoefficient": "1.0000"
    },
    {
        "Activity_Date": "2019-05-07",
        "Open": "366.3300",
        "High": "367.7100",
        "Low": "355.0200",
        "Close": "357.2300",
        "AdjustedClose": "357.2300",
        "Volume": "9733702",
        "DividendAmount": "0.0000",
        "SplitCoefficient": "1.0000"
    },
    {
        "Activity_Date": "2019-05-06",
        "Open": "367.8800",
        "High": "372.4800",
        "Low": "365.6300",
        "Close": "371.6000",
        "AdjustedClose": "371.6000",
        "Volume": "4747601",
        "DividendAmount": "0.0000",
        "SplitCoefficient": "1.0000"
    },

…

    {
        "Activity_Date": "2018-12-14",
        "Open": "322.4500"
        "High": "323.9100"
        "Low": "315.5600",
        "Close": "318.7500",
        "AdjustedClose": "317.1415",
        "Volume": "3298436",
        "DividendAmount": "0.0000",
        "SplitCoefficient": "1.0000"
     },
        "Activity_Date": "2018-12-13"
        "Open": "328.4000",
        "High": "328.7400",
        "Low": "324.1730",
        "Close": "325.4700",
        "AdjustedClose": "323.8276",
        "Volume": "2247706",
        "DividendAmount": "0.0000",
        "SplitCoefficient": "1.0000"
     }
   ]
}

This fragment represents a root object and two child objects.

  1. Meta_Data is a scalar object that has five properties, each represented by a name-value pair.
  2. Time_Series_Daily is an array that contains an arbitrary number of scalar objects, each of which has nine properties, also represented by name-value pairs.

The JSON string returned by the REST API endpoint looks like the following fragmen.

JavaScript
{
   "Meta Data": {
      "1. Information": "Daily Time Series with Splits and Dividend Events",
      "2. Symbol": "BA",
      "3. Last Refreshed": "2019-05-08 16:00:44",
      "4. Output Size": "Compact",
      "5. Time Zone": "US/Eastern"
   },
      "Time Series (Daily)": {
           "2019-05-08": {
               "1. open": "357.7700",
               "2. high": "361.5200",
               "3. low": "353.3300",
               "4. close": "359.7500",
               "5. adjusted close": "359.7500",
               "6. volume": "5911593",
               "7. dividend amount": "0.0000",
               "8. split coefficient": "1.0000"
           },
           "2019-05-07": {
               "1. open": "366.3300",
               "2. high": "367.7100",
               "3. low": "355.0200",
               "4. close": "357.2300",
               "5. adjusted close": "357.2300",
               "6. volume": "9733702",
               "7. dividend amount": "0.0000",
               "8. split coefficient": "1.0000"
           },
…

           "2018-12-14": {
               "1. open": "322.4500",
               "2. high": "323.9100",
               "3. low": "315.5600",
               "4. close": "318.7500",
               "5. adjusted close": "317.1415",
               "6. volume": "3298436",
               "7. dividend amount": "0.0000",
               "8. split coefficient": "1.0000"
            },
            "2018-12-13": {
               "1. open": "328.4000",
               "2. high": "328.7400",
               "3. low": "324.1730",
               "4. close": "325.4700",
               "5. adjusted close": "323.8276",
               "6. volume": "2247706",
               "7. dividend amount": "0.0000",
               "8. split coefficient": "1.0000"
           }
     ]
}

There are several things wrong with the JSON string shown above.

  1. Most of the object and property names contain embedded spaces, parentheses, full stops, and other characters that are invalid in the name of a C# variable.
  2. The Time Series (Daily) elements, destined to become the Time_Series_Daily array, are organized as a set of named properties on the Time_Series_Daily object, each of which is a scalar object that sports eight properties.
  3. The names of the eight properties begin with a numeral, which is invalid as the first character in the name of a C# variable.

Even if you are willing to settle for a dynamically generated object, processing that object would be cumbersome. The data set is a time series of stock market data that begs to be treated as an array of data points. With a few transformations, most of which are straightforward, the string returned by the REST endpoint can be converted into something that is easy to deserialize into an array that can be processed very efficiently.

The other knowledge that is essential to solving this puzzle is that the Paste Special feature of the Visual Studio code editor can transform well-formed XML or JSON into a class definition.

  1. Copy the JSON (or XML) into the Windows Clipboard.
  2. Use the Solution Explorer to define a new class.
  3. Use the Paste Special tool on the Edit menu to paste the string into the empty class, replacing the entire class definition. Leave the namespace block.
  4. The editor names the outermost element (class) RootObject. Rename it to match the name that you gave to the empty class when you created it., While you can get away without renaming the root object if you create only one class by this technique, leaving it is a stinky code smell.

Using the code

The demonstration package that accompanies this article is the GitHub repository at https://github.com/txwizard/LineBreakFixupsDemo/. Instructions for setting up and using the code are in the like-named section of Line Breaks: From Windows to Unix and Back, and I recommend that you follow them to the letter even if you intend to run through it in the Visual Studio debugger. My reason for recommending that you follow the instructions to the letter is that, although the dependencies can be acquired from the NuGet Gallery, and the project can be built from the supplied source, the test data files must be extracted from a ZIP file as directed, so that it is where the demonstration program expects to find them.

You may run the program from a command prompt, the Run box, the File Explorer, or the Visual Studio debugger, or you may use a command line argument, TransformJSONString, to run the JSON conversion demonstration discussed in this article by itself. The argument can be appended to the command string in a command prompt window or the Run box, or it can be added to the Debug tab of the Visual Studio project property editor.

Points of Interest

The conversion happens in two passes (three, counting the line break fixups discussed in Line Breaks: From Windows to Unix and Back). Static method PerformJSONTransofmration, shown below, and defined in Program.cs, oversees the transformation, deserialization, and generation of a report that lists the time series data points.

C#
private static int PerformJSONTransofmration (
    int pintTestNumber ,
    bool pfConvertLineEndings ,
    string pstrTestReportLabel ,
    string pstrRESTResponseFileName ,
    string pstrIntermediateFileName ,
    string pstrFinalOutputFileName ,
    string pstrResponseObjectFileName )
{
    Utl.BeginTest (
        pstrTestReportLabel ,
        ref pintTestNumber );
    string strRawResponse = pfConvertLineEndings
        ? Utl.GetRawJSONString ( pstrRESTResponseFileName ).UnixLineEndings ( )
        : Utl.GetRawJSONString ( pstrRESTResponseFileName );
    JSONFixupEngine engine = new JSONFixupEngine ( @"TIME_SERIES_DAILY_ResponseMap" );
    string strFixedUp_Pass_1 = engine.ApplyFixups_Pass_1 ( strRawResponse );
    Utl.PreserveResult (
        strFixedUp_Pass_1                                   // string pstrPreserveThisResult
        pstrIntermediateFileName ,                          // string pstrOutputFileNamePerSettings
        Properties.Resources.FILE_LABEL_INTERMEDIATE );     // string pstrLabelForReportMessage
    string strFixedUp_Pass_2 = engine.ApplyFixups_Pass_2 ( strFixedUp_Pass_1 );
    Utl.PreserveResult (
        strFixedUp_Pass_2 ,                                 // string pstrPreserveThisResult
        pstrFinalOutputFileName ,                           // string pstrOutputFileNamePerSettings
        Properties.Resources.FILE_LABEL_FINAL );            // string pstrLabelForReportMessage
    //  ------------------------------------------------------------
    // TimeSeriesDailyResponse<
    //  ------------------------------------------------------------

    Utl.ConsumeResponse (
        pstrResponseObjectFileName ,
        Newtonsoft.Json.JsonConvert.DeserializeObject<TimeSeriesDailyResponse> (
            strFixedUp_Pass_2 ) );
    s_smThisApp.BaseStateManager.AppReturnCode = Utl.TestDone (
        MagicNumbers.ERROR_SUCCESS ,
        pintTestNumber );
    return pintTestNumber;
}    // private static int PerformJSONTransofmration

This method is mostly straight-line code.

  1. Static method Utl.GetRawJSONString reads the JSON response returned by the REST endpoint into a string from a text file. If the input file has passed through any process, such as a Git client, that might have replaced the expected Unix line breaks with Windows line breaks, the UnixLineEndings extension method is chained to the method, so that strRawResponse is guaranteed to have Unix line breaks. In a production application, I would expect to assume nothing, and always call UnixLineEndings.
  2. A new JSONFixupEngine object is constructed. Its string argument, @"TIME_SERIES_DAILY_ResponseMap", is the name of an embedded text file resource that contains the list of string substitution pairs, about which I shall explain shortly.
  3. Instance method ApplyFixups_Pass_1 takes strRawResponse as input and returns strFixedUp_Pass_1 (love the imaginative names, eh?). This method wraps a call to the ApplyFixups method on private instance member _responseStringFixups, a StringFixups object that the constructor creates from the substitution pairs that it reads from embedded text file resource TIME_SERIES_DAILY_ResponseMap.TXT.
  4. Instance method ApplyFixups_Pass_2 takes strFixedUp_Pass_1 as input, returning strFixedUp_Pass_2.
  5. Static method Utl.ConsumeResponse takes string argument pstrResponseObjectFileName, the name to assign to the tab-delimited report file, and the TimeSeriesDailyResponse object returned by feeding string strFixedUp_Pass_2 into static method Newtonsoft.Json.JsonConvert.DeserializeObject<T>. This method returns void.

Between each step described above, static void method Utl.PreserveResult writes the output string that was just created into a new text file, then prints the file name and other statistics onto the console log.

ApplyFixups_Pass_1: The First Transformation

The first transformation uses the substitution string pairs shown in Table 1, which the JSONFixupEngine constructor stores into the array of StringFixups.StringFixup structures that it constructs from the embedded text file resource and feeds into the StringFixups constructor. Ultimately, StringFixups is a container for an array of StringFixup structures that is fed by its own ApplyFixups method to the like-named extension method on its string argument.

The StringFixups constructor is noteworthy for its use of LoadStringFixups, reproduced below, which, in turn, combines LoadTextFileFromEntryAssembly, a static method on class WizardWrx.EmbeddedTextFile.Readers, and a WizardWrx.AnyCSV.Parser instance to split an embedded text file into an array of StringFixups.StringFixup structures.

C#
private static StringFixups.StringFixup [ ] LoadStringFixups ( string pstrEmbeddedResourceName )
{
    const string LABEL_ROW = @"JSON     VS";
    const string TSV_EXTENSION = @".txt";

    const int STRING_PER_RESPONSE = ArrayInfo.ARRAY_FIRST_ELEMENT;
    const int STRING_FOR_JSONCONVERTER = STRING_PER_RESPONSE + ArrayInfo.NEXT_INDEX;
    const int EXPECTED_FIELD_COUNT = STRING_FOR_JSONCONVERTER + ArrayInfo.NEXT_INDEX;

    string strEmbeddResourceFileName = string.Concat (
        pstrEmbeddedResourceName ,
        TSV_EXTENSION );

    string [ ] astrAllMapItems = Readers.LoadTextFileFromEntryAssembly ( strEmbeddResourceFileName );
    Parser parser = new Parser (
        CSVParseEngine.DelimiterChar.Tab ,
        CSVParseEngine.GuardChar.DoubleQuote ,
        CSVParseEngine.GuardDisposition.Strip );
    StringFixups.StringFixup [ ] rFunctionMaps = new StringFixups.StringFixup [ ArrayInfo.IndexFromOrdinal ( astrAllMapItems.Length ) ];

    for ( int intI = ArrayInfo.ARRAY_FIRST_ELEMENT ;
              intI < astrAllMapItems.Length ;
              intI++ )
    {
        if ( intI == ArrayInfo.ARRAY_FIRST_ELEMENT )
        {
            if ( astrAllMapItems [ intI ] != LABEL_ROW )
            {
                throw new Exception (
                    string.Format (
                        Properties.Resources.ERRMSG_CORRUPTED_EMBBEDDED_RESOURCE_LABEL ,
                        new string [ ]
                        {
                            strEmbeddResourceFileName ,     // Format Item 0: internal resource {0}
                            LABEL_ROW ,                     // Format Item 1: Expected value = {1}
                            astrAllMapItems [ intI ] ,      // Format Item 2: Actual value   = {2}
                            Environment.NewLine             // Format Item 3: Platform-specific newline
                        } ) );
            }   // if ( astrAllMapItems[intI] != LABEL_ROW )
        }   // TRUE (label row sanity check 1 of 2) block, if ( intI == ArrayInfo.ARRAY_FIRST_ELEMENT )
        else
        {
            string [ ] astrFields = parser.Parse ( astrAllMapItems [ intI ] );

            if ( astrFields.Length == EXPECTED_FIELD_COUNT )
            {
                rFunctionMaps [ ArrayInfo.IndexFromOrdinal ( intI ) ] = new StringFixups.StringFixup (
                    astrFields [ STRING_PER_RESPONSE ] ,
                    astrFields [ STRING_FOR_JSONCONVERTER ] );
            }   // TRUE (anticipated outcome) block, if ( astrFields.Length == EXPECTED_FIELD_COUNT )
            else
            {
                throw new Exception (
                    string.Format (
                        Properties.Resources.ERRMSG_CORRUPTED_EMBEDDED_RESOURCE_DETAIL ,
                        new object [ ]
                        {
                            intI ,                          // Format Item 0: Detail record {0}
                            strEmbeddResourceFileName ,     // Format Item 1: internal resource {1}
                            EXPECTED_FIELD_COUNT ,          // Format Item 2: Expected field count = {2}
                            astrFields.Length ,             // Format Item 3: Actual field count   = {3}
                            astrAllMapItems [ intI ] ,      // Format Item 4: Actual record        = {4}
                            Environment.NewLine             // Format Item 5: Platform-specific newline
                        } ) );
            }   // FALSE (unanticipated outcome) block, if ( astrFields.Length == EXPECTED_FIELD_COUNT )
        }   // FALSE (detail row) block, if ( intI == ArrayInfo.ARRAY_FIRST_ELEMENT )
    }   // for ( int intI = ArrayInfo.ARRAY_FIRST_ELEMENT ; intI < astrAllMapItems.Length ; intI++ )

    return rFunctionMaps;
}   // private static StringFixups.StringFixup [ ] GetSStringFixups

As insurance against data corruption, LoadStringFixups sanity checks the label row and each detail row in the input file. If anything is amiss, an exception arises, and is expected to be caught and reported, since the accompanying message supplies significant detail that is intended to pinpoint the source of the corruption.

With respect to my decision to embed the file in the assembly, it was one fewer thing to manage. The file lives in the source code folder, belongs to the project, and is marked as Embedded Resource content. Every time the project is built, the text file is copied into the assembly, so that it is always there when the program executes.

ArrayInfo.ARRAY_FIRST_ELEMENT and ArrayInfo.NEXT_INDEX, both defined in WizardWrx.Common.dll, and exported into the root WizardWrx namespace, and several constants exported by WizardWrx.AnyCSV.dll to initialize local constants defined and used by LoadStringFixups. These are but a few of many constants, most of which belong to the numerous static classes exported into the root WizardWrx namespace by WizardWrx.Common.dll, which is available as a NuGet package, WizardWrx.Common. In addition to these constants, the managed string resources defined in WizardWrx.Common.dll are marked as Public, so that any assembly can use them.

These constants and public strings are huge labor-savers, not to mention the improvement they bring in code readability.

Table 1 lists the string substitution pairs. The strings in the left column, labeled JSON, are the strings returned by the REST endpoint. The strings in the right column, labeled VS, are the valid variable names that replace them.

JSON

VS

Meta Data

Meta_Data

1. Information

Information

2. Symbol

Symbol

3. Last Refreshed

LastRefreshed

4. Output Size

OutputSize

5. Time Zone

TimeZone

Time Series (Daily)

TimeSeriesDaily

1. open

Open

2. high

High

3. low

Low

4. close

Close

5. adjusted close

AdjustedClose

6. volume

Volume

7. dividend amount

DividendAmount

8. split coefficient

SplitCoefficient

String extension method ApplyFixups is a straightforward for loop that iterates over the StringFixup array that is passed into it, applying each element in turn to the input string. The first iteration initializes output string rstrFixedUp from argument pstrIn, which subsequent iterations take as their input, returning a new rstrFixedUp string.

C#
public static string ApplyFixups (
    this string pstrIn ,
    WizardWrx.Core.StringFixups.StringFixup [ ] pafixupPairs )
{
    string rstrFixedUp = null;

    for ( int intFixupIndex = ArrayInfo.ARRAY_FIRST_ELEMENT ;
              intFixupIndex < pafixupPairs.Length ;
              intFixupIndex++ )
    {
        if ( intFixupIndex == ArrayInfo.ARRAY_FIRST_ELEMENT )
        {
            rstrFixedUp = pstrIn.Replace (
                pafixupPairs [ intFixupIndex ].InputValue ,
                pafixupPairs [ intFixupIndex ].OutputValue );
        }   // TRUE (On the first pass, the output string is uninitialized.) block, if ( intFixupIndex == ArrayInfo.ARRAY_FIRST_ELEMENT )
        else
        {
            rstrFixedUp = rstrFixedUp.Replace (
                pafixupPairs [ intFixupIndex ].InputValue ,
                pafixupPairs [ intFixupIndex ].OutputValue );
        }   // FALSE (Subsequent passes must feed the output string through its Replace method with the next StringFixup pair.) block, if ( intFixupIndex == ArrayInfo.ARRAY_FIRST_ELEMENT )
    }   // for ( int intFixupIndex = ArrayInfo.ARRAY_FIRST_ELEMENT ; intFixupIndex < _afixupPairs.Length ; intFixupIndex++ )

    return rstrFixedUp;
}   // ApplyFixups method

ApplyFixups_Pass_2: The Second Transformation

JSONFixupEngine instance method ApplyFixups_Pass_2, which performs the second phase of the string transformation, is a bit less straightforward, because the first iteration of the central while loop that is responsible for most of its processing uses a Boolean state flag property, _fIsFirstPass, to alter its behavior on subsequent iterations. The whole method body is shown next.

C#
public string ApplyFixups_Pass_2 ( string pstrFixedUp_Pass_1 )
{   // This method references and updates instance member __fIsFirstPass.
    const string TSD_LABEL_ANTE = "\"TimeSeriesDaily\": {"		// Ante: "TimeSeriesDaily": {
	const string TSD_LABEL_POST = "\"Time_Series_Daily\" : ["	// Post: "Time_Series_Daily": [

	const string END_BLOCK_ANTE = "}\n    }\n}";
	const string END_BLOCK_POST = "}\n    ]\n}";

	const int DOBULE_COUNTING_ADJUSTMENT = MagicNumbers.PLUS_ONE;					// Deduct one from the length to account for the first character occupying the position where copying begins.

	_fIsFirstPass = true															// Re-initialize the First Pass flag.

	StringBuilder builder1 = new StringBuilder ( pstrFixedUp_Pass_1.Length * MagicNumbers.PLUS_TWO );

	builder1.Append (
		pstrFixedUp_Pass_1.Replace (
		TSD_LABEL_ANTE ,
		TSD_LABEL_POST ) );
	int intLastMatch = builder1.ToString ( ).IndexOf ( TSD_LABEL_POST )
		+ TSD_LABEL_POST.Length
		- DOBULE_COUNTING_ADJUSTMENT;

	while ( intLastMatch > ListInfo.INDEXOF_NOT_FOUND )
	{
		intLastMatch = FixNextItem (
			builder1 ,
			intLastMatch );
	}	// while ( intLastMatch > ListInfo.INDEXOF_NOT_FOUND )

	//  ----------------------------------------------------------------
	//  Close the array by replacing the last French brace with a square
	//	bracket.
	//  ----------------------------------------------------------------

	builder1.Replace (
		END_BLOCK_ANTE ,
		END_BLOCK_POST );

	return builder1.ToString ( );
}	// public string ApplyFixups_Pass_2

Unlike ApplyFixups_Pass_1, this method employs two hard coded string pairs, the first of which is applied once only to transform TimeSeriesDaily, the intermediate name applied to the property that becomes the Time_Series_Daily array in the final JSON string. In addition to giving the property its final name, this one-off transformation converts it into an array by replacing its opening { character with a [ character.

Initializing intLastMatch so that the loop ignores the beginning of the string, which it is no longer necessary (and is, indeed, wasteful) to search requires the StringBuilder to be temporarily transformed into a string, because StringBuilder lacks an IndexOf method. This wasted motion could be eliminated by defining a StringBuilder extension method (named IndexOf, no doubt). Since this method came into being to meet a one-off requirement, that task was set aside, as it would have required yet another addition to the WizardWrx.Core library.

Following the one-off transformation that creates the array, control falls into the main while loop, which executes until private method FixNextItem returns ListInfo.INDEXOF_NOT_FOUND (-1), indicating that the last array element has been found and fixed.

Instance method FixNextItem is generally like its caller, except that the replacement task is handed off to another instance method, FixNextItem, through which it returns.

C#
private int FixNextItem (
 	StringBuilder pbuilder ,
	 int pintLastMatch )
{ 	// This method references private instance member _fIsFirstPass several times.
	const string FIRST_ITEM_BREAK_ANTE = "[\n \""; // Ante: },\n "
	const string SUBSEQUENT_ITEM_BREAK_ANTE = "},\n \""; // Ante: },\n "

	string strInput = pbuilder.ToString ( );
	int intMatchPosition = strInput.IndexOf (
	_fIsFirstPass
		? FIRST_ITEM_BREAK_ANTE
		: SUBSEQUENT_ITEM_BREAK_ANTE ,
		pintLastMatch );

	if ( intMatchPosition > ListInfo.INDEXOF_NOT_FOUND )
	{
		return FixThisItem (
			strInput ,
			intMatchPosition ,
			_fIsFirstPass
				? FIRST_ITEM_BREAK_ANTE.Length
				: SUBSEQUENT_ITEM_BREAK_ANTE.Length ,
			pbuilder );
	} // TRUE (At least one match remains.) block, if ( intMatchPosition > ListInfo.INDEXOF_NOT_FOUND )
	else
	{
		return ListInfo.INDEXOF_NOT_FOUND;
	} // FALSE (All matches have been found.) block, if ( intMatchPosition > ListInfo.INDEXOF_NOT_FOUND )
} // private int FixNextItem

Without digging more deeply into the workings of StringBuilder instances, and probably writing and testing one or more extension methods, I was forced to absorb the cost of converting the StringBuilder that is fed into FixNextItem into a new string and passing it into FixThisItem.

Functionally, FixThisItem transforms the Activity_Date property on the Time_Series_Daily object into a property of the Time_Series_Daily array. As it returns, it is also responsible for turning off the _fIsFirstPass state flag. While doing this on every iteration is overkill, I couldn’t find another way that wouldn’t require it to be wrapped in a test; I decided it was computationally cheaper to set it on every iteration. Static method ArrayInfo.OrdinalFromIndex increments its input value, causing the return value to point to the character that immediately follows the last substitution, which is where the next string scan begins. ArrayInfo is a static class exported into the root WizardWrx namespace by WizardWrx.Core.dll.

C#
private int FixThisItem (
	string pstrInput ,
	int pintMatchPosition ,
	int pintMatchLength ,
	StringBuilder psbOut )
{
	const string FIRST_ITEM_BREAK_POST = "\n {\n \"Activity_Date\": \""; // Post: },\n {\n {\n "Activity_Date": "
	const string SUBSEQUENT_ITEM_BREAK_POST = ",\n {\n \"Activity_Date\": \""; // Post: },\n {\n {\n "Activity_Date": "

	const int DATE_TOKEN_LENGTH = 11;
	const int DATE_TOKEN_SKIP_CHARS = DATE_TOKEN_LENGTH + 3;

	int intSkipOverMatchedCharacters = pintMatchPosition + pintMatchLength;

	psbOut.Clear ( );

	psbOut.Append ( pstrInput.Substring (
		ListInfo.SUBSTR_BEGINNING ,
		ArrayInfo.OrdinalFromIndex ( pintMatchPosition ) ) );
		psbOut.Append ( _fIsFirstPass
			? FIRST_ITEM_BREAK_POST
			: SUBSEQUENT_ITEM_BREAK_POST );
	psbOut.Append ( pstrInput.Substring (
		intSkipOverMatchedCharacters ,
		DATE_TOKEN_LENGTH ) );
	psbOut.Append ( SpecialCharacters.COMMA );
	psbOut.Append ( pstrInput.Substring ( intSkipOverMatchedCharacters + DATE_TOKEN_SKIP_CHARS ) );

	int rintSearchResumePosition = pintMatchPosition
		+ ( _fIsFirstPass
			? FIRST_ITEM_BREAK_POST.Length
			: SUBSEQUENT_ITEM_BREAK_POST.Length );
	_fIsFirstPass = false; // Putting this here allows execution to be unconditional.

	return ArrayInfo.OrdinalFromIndex ( rintSearchResumePosition );
} // private int FixThisItem

Finally, a second one-off replacement completes the task of converting the Time_Series_Daily property into an array by exchanging its closing } for a ].

ConsumeResponse

The final phase, ConsumeResponse, reports the properties on the main TimeSeriesDailyResponse object returned by the JSON deserializer. Since there are many points of interest, the whole method is reproduced next.

C#
internal static void ConsumeResponse (
	string pstrReportFileName ,
	TimeSeriesDailyResponse timeSeriesDailyResponse )
{
	Console.WriteLine (
		Properties.Resources.MSG_RESPONSE_METADATA , // Format control string
		new object [ ]
		{
			timeSeriesDailyResponse.Meta_Data.Information , 	// Format item 0: Information = {0}
			timeSeriesDailyResponse.Meta_Data.Symbol , 			// Format Item 1: Symbol = {1}
			timeSeriesDailyResponse.Meta_Data.LastRefreshed , 	// Format Item 2: LastRefreshed = {2}
			timeSeriesDailyResponse.Meta_Data.OutputSize , 		// Format Item 3: OutputSize = {3}
			timeSeriesDailyResponse.Meta_Data.TimeZone , 		// Format Item 4: TimeZone = {4}
			timeSeriesDailyResponse.Time_Series_Daily.Length , 	// Format Item 5: Detail Count = {5}
			Environment.NewLine 								// Format Item 6: Platform-dependent newline
		} );

	string strAbsoluteInputFileName = AssembleAbsoluteFileName ( pstrReportFileName );

	using ( StreamWriter swTimeSeriesDetail = new StreamWriter ( strAbsoluteInputFileName ,
		FileIOFlags.FILE_OUT_CREATE ,
		System.Text.Encoding.ASCII ,
		MagicNumbers.CAPACITY_08KB ) )
	{
		string strLabelRow = Properties.Resources.MSG_RESPONSE_DETAILS_LABELS.ReplaceEscapedTabsInStringFromResX ( );
		swTimeSeriesDetail.WriteLine ( strLabelRow );
		string strDetailRowFormatString = ReportHelpers.DetailTemplateFromLabels ( strLabelRow );

		for ( int intJ = ArrayInfo.ARRAY_FIRST_ELEMENT ;
			intJ < timeSeriesDailyResponse.Time_Series_Daily.Length ;
			intJ++ )
		{
			Time_Series_Daily daily = timeSeriesDailyResponse.Time_Series_Daily [ intJ ];
			swTimeSeriesDetail.WriteLine (
			strDetailRowFormatString ,
			new object [ ]
			{
				ArrayInfo.OrdinalFromIndex ( intJ ) , 			// Format Item 0: Item
				Beautify ( daily.Activity_Date) , 				// Format Item 1: Activity_Date
				Beautify ( daily.Open ) , 						// Format Item 2: Open
				Beautify ( daily.High ) , 						// Format Item 3: High
				Beautify ( daily.Low ) , 						// Format Item 4: Low
				Beautify ( daily.Close ) , 						// Format Item 5: Close
				Beautify ( daily.AdjustedClose ) , 				// Format Item 6: AdjustedClose
				Beautify ( daily.Volume ) , 					// Format Item 7: Volume
				Beautify ( daily.DividendAmount ) , 			// Format Item 8: DividendAmount
				Beautify ( daily.SplitCoefficient ) 			// Format Item 9: SplitCoefficient
			} );
		} // for ( int intJ = ArrayInfo.ARRAY_FIRST_ELEMENT ; intJ < timeSeriesDailyResponse.Time_Series_Daily.Length ; intJ++ )
	} // using ( StreamWriter swTimeSeriesDetail = new StreamWriter ( strAbsoluteInputFileName , FileIOFlags.FILE_OUT_CREATE , System.Text.Encoding.ASCII , MagicNumbers.CAPACITY_08KB ) )

	Console.WriteLine (
		ShowFileDetails ( 										// Print the returned string.
			Properties.Resources.FILE_LABEL_CONTENT_REPORT , 	// string pstrLabel
			strAbsoluteInputFileName , 							// string pstrFileName
			true , 												// bool pfPrefixWithNewline = false
			false ) ); 											// bool pfSuffixWithNewline = true
} // private static void ConsumeResponse
  1. The only remarkable feature of the first print statement is the line comments that I appended to associate each item in the parameter array with the format item through which it makes its way into the output. This ingrained habit has saved much grief by helping me catch many errors in the construction of WriteLine statements as the code is written.
  2. String strLabelRow uses ReplaceEscapedTabsInStringFromResX, another extension method, to clean up the double backslashes that are required to embed tabs in a managed string resource.
  3. ReportHelpers.DetailTemplateFromLabels, exported into the root WizardWrx namespace by WizardWrx.Core.dll, generates the format control string required to write everything in the tab delimited strings that follow a label row into a tab delimited text file. This method also has an overload that allows the delimiter to be specified. Using this method eliminates entirely the tedious, error-prone process of writing these format control strings.
  4. The next block is a conventional for loop that iterates through the array of Time_Series_Daily elements. The WriteLine statement is laid out along the lines described in item 1.
  5. Finally, utility method ShowFileDetails is called upon to report the file details on the program output console. ShowFileDetails constructs a FileInfo object, then invokes a like-named extension method that returns a formatted report, which is fed into Console.WriteLine.

Extension method ShowFileDetails is exported into the root WizardWrx namespace by WizardWrx.Core.dll, which is available as a like-named NuGet package.

History

Sunday, 23 June 2019: Initial Publication

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)